FastCloner

repository·next·Indexed 18 days ago

https://github.com/lofcz/fastcloner

A high-performance, zero-dependency deep cloning library for .NET supporting versions 4.6 through 10+. It offers a hybrid approach featuring a reflection-based engine via FastCloner.DeepClone() and an AOT-friendly source generator via FastCloner.SourceGenerator and the [FastClonerClonable] attribute. The library includes advanced controls for clone behavior, member visibility, identity preservation, and a specialized Internalization Builder for embedding the functionality directly into other libraries.

Tokens
6.1K
Snippets
18
Records
23
Agent score
63%

What's inside FastCloner

  1. CI Integration for FastCloner Benchmarks

    next

    The benchmark suite is integrated into GitHub Actions via .github/workflows/benchmark.yml.

    Workflow Behavior

    • Push/PR to next branch: Runs automatically on ubuntu-latest.
    • Manual Trigger: Uses workflow_dispatch, allowing users to select the target OS (Windows, Ubuntu, or macOS) via checkboxes.
    • PR Comments: Automatically posts a comment containing:
      • A comparison table of FastCloner vs DeepCloner.
      • A diff showing regressions or improvements against the latest successful baseline from master.

    Baseline Resolution Logic

    When running in a PR, the system resolves the baseline in this order:

    1. Uses the latest successful uploaded baseline artifact for the target OS from the master branch.
    2. If no artifact is available, it performs a "slow path": clones master, runs the full benchmark suite, and generates a fresh normalized baseline before performing the comparison.
  2. FastCloner.SourceGenerator features and requirements

    next

    Features

    • Zero reflection: All cloning logic is generated at compile time.
    • Circular reference detection: Automatically handled.
    • High performance: Optimized for speed.
    • Collections support: Supports List, Dictionary, Arrays, etc.
    • Nullable reference types: Full support.
    • No partial requirement: Works with any class or struct without requiring the partial keyword.

    Requirements

    • Framework: .NET Standard 2.0+ or .NET 5+
    • Language: C# 9.0+
  3. Performance and Build Efficiency of FastCloner

    next

    FastCloner is optimized for high performance in both reflection-based and AOT (Ahead-of-Time) workloads.

    Source Generator Design

    The source generator is built using Roslyn's incremental model to ensure minimal impact on IDE performance and build times:

    • Incremental pipeline: Type analysis only re-runs when decorated types or relevant usage data changes.
    • Stable models: Uses TypeModel and MemberModel records to hold precomputed data instead of Roslyn symbols, making incremental caching effective.
    • Reduced invalidation: The output pipeline avoids using CompilationProvider to prevent broad invalidation and unnecessary regeneration.
    • Deterministic equality: Uses EquatableArray for model collection comparisons in the incremental pipeline.
    • Direct paths: One-off helpers are inlined to keep generated clone paths direct.
  4. Customize clone behavior with attributes or runtime settings

    next

    FastCloner allows you to control how specific types or members are cloned using CloneBehavior.

    Available Behaviors

    • Clone: Deep recursive copy (default).
    • Reference: Return the original instance unchanged.
    • Shallow: Performs a MemberwiseClone without recursion.
    • Ignore: Returns default (e.g., null for reference types).

    Applying Behavior

    1. Compile-time (Attributes)

    You can apply attributes to types or members. Member-level attributes override type-level settings.

    Shorthand Attributes:

    • [FastClonerIgnore]
    • [FastClonerShallow]
    • [FastClonerReference]

    Explicit Attribute:

    • [FastClonerBehavior(CloneBehavior.X)]

    2. Runtime (Reflection only)

    You can configure behavior dynamically. Runtime settings have the highest precedence and are checked before attributes.

    FastCloner.FastCloner.SetTypeBehavior<MySingleton>(CloneBehavior.Reference);
    FastCloner.FastCloner.ClearTypeBehavior<MySingleton>();    // Reset one
    FastCloner.FastCloner.ClearAllTypeBehaviors();             // Reset all

    Note: Changing runtime behavior invalidates the cache. Configure once at startup if possible.

    Precedence (Highest to Lowest)

    1. Runtime SetTypeBehavior<T>()
    2. Member-level attribute
    3. Type-level attribute on member's type
    4. Default behavior
    [FastClonerReference]  // Type-level: all usages preserve reference
    public class SharedService { }
    
    public class MyClass
    {
        public SharedService Svc { get; set; }      // Uses type-level → Reference
        
        [FastClonerBehavior(CloneBehavior.Clone)]   // Member-level override → Clone
        public SharedService ClonedSvc { get; set; }
        
        [FastClonerIgnore]                          // → null/default
        public CancellationToken Token { get; set; }
        
        [FastClonerShallow]                         // → Reference copied directly
        public ParentNode Parent { get; set; }
    }
  5. Performance characteristics of FastCloner

    next

    FastCloner is designed to prioritize correctness, particularly in complex scenarios like deep cloning dictionaries where other libraries may fail.

    It operates in two modes:

    1. Reflection Mode (Default): Uses reflection to perform cloning. It is highly competitive within the reflection-based category but generally slower than IL generation or source generators.
    2. Source Generator Mode (Opt-in): Provides increased performance by generating cloning code at compile time.

    Note that FastCloner may allocate some extra memory upfront to maintain various lookup tables, which is a one-time cost to improve subsequent cloning operations.

  6. How generic classes and abstract types are handled

    next

    The FastCloner source generator automatically discovers and generates specialized cloning code for:

    • Generic types: The generator scans for usages (e.g., MyClass<int>) and generates specific code for those concrete instantiations.
    • Abstract classes: The generator automatically finds all concrete derived types in your codebase to ensure they can be cloned via the base type.
    [FastClonerClonable]
    public abstract class Animal
    {
        public string Name { get; set; }
    }
    
    public class Dog : Animal
    {
        public string Breed { get; set; }
    }
    
    // Cloning via the abstract type works
    Animal pet = new Dog { Name = "Buddy", Breed = "Labrador" };
    Animal clone = pet.FastDeepClone(); // Returns a cloned Dog
    [FastClonerClonable]
    public abstract class Animal
    {
        public string Name { get; set; }
    }
    
    public class Dog : Animal
    {
        public string Breed { get; set; }
    }
    
    Animal pet = new Dog { Name = "Buddy", Breed = "Labrador" };
    Animal clone = pet.FastDeepClone();
  7. Use FastCloner.SourceGenerator for deep cloning

    next

    To use the source generator, follow these two steps:

    1. Decorate the classes or structs you want to clone with the [FastClonerClonable] attribute from the FastCloner.SourceGenerator.Shared namespace.
    2. Call the generated FastDeepClone() extension method on an instance of that type.

    Note: Unlike many other source generators, you do not need to declare your classes as partial.

    using FastCloner.SourceGenerator.Shared;
    
    [FastClonerClonable]
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public List<string> Hobbies { get; set; }
    }
    
    // Usage:
    var original = new Person { Name = "John", Age = 30, Hobbies = new() { "Reading" } };
    var clone = original.FastDeepClone();
  8. Optimize hash-based collections with [FastClonerStableHash]

    next

    For types used as keys in HashSet<T> or Dictionary<TKey, TValue>, you can use [FastClonerStableHash] to skip the runtime probe that determines if GetHashCode is value-based.

    Warning: Only use this if GetHashCode is a pure function of the type's fields. Do not use it if GetHashCode depends on object identity (e.g., RuntimeHelpers.GetHashCode(this)), as the cloned collection will be unable to find its own contents.

    [FastClonerStableHash]
    public sealed class CompositeKey
    {
        public int Major { get; }
        public int Minor { get; }
        public override int GetHashCode() => HashCode.Combine(Major, Minor);
        public override bool Equals(object? obj) 
            => obj is CompositeKey other && other.Major == Major && other.Minor == Minor;
    }
  9. Embed FastCloner directly via the Internalization Builder

    next

    If you want to include FastCloner's functionality directly within your own library without adding it as a formal dependency, you can use the FastCloner.Internalization.Builder project. This tool rewrites namespaces, handles preprocessor directives, and adjusts visibility to fit your target project.

    To use it, run the builder project from your terminal using dotnet run and provide the necessary configuration flags.

    dotnet run --project src/FastCloner.Internalization.Builder/FastCloner.Internalization.Builder.csproj -- \
      --root-namespace MyLibrary.FastCloner \
      --output ../MyLibrary/FastCloner \
      --preprocessor "MODERN=true;" \
      --fqn all \
      --visibility internal \
      --public-api none \
      --runtime-only true \
      --self-check
  10. Explicitly include types for cloning

    next

    If a type is used dynamically and is not visible at compile time (e.g., in an external assembly or via reflection), use the [FastClonerInclude] attribute to ensure the source generator creates cloning code for it.

    This is useful for:

    1. Including types used dynamically within a generic wrapper.
    2. Adding derived types from external assemblies to an abstract class hierarchy.
    [FastClonerClonable]
    [FastClonerInclude(typeof(Customer), typeof(Order))]
    public class Wrapper<T>
    {
        public T Value { get; set; }
    }
    
    [FastClonerClonable]
    [FastClonerInclude(typeof(ExternalPlugin))]
    public abstract class Plugin
    {
        public string Name { get; set; }
    }
  11. Preserve object identity with [FastClonerPreserveIdentity]

    next

    By default, FastCloner does not track object identity, meaning shared references in a graph will become separate instances in the clone. To ensure shared references remain shared (e.g., clone.Author == clone.LastEditor), use [FastClonerPreserveIdentity].

    • Type-level: Enables identity tracking for all members of the type.
    • Member-level: Enables identity tracking only for a specific member.
    • Opt-out: You can disable identity tracking for a specific member using [FastClonerPreserveIdentity(false)] even if the type has it enabled.

    Note: Identity preservation adds overhead for tracking seen objects. Circular references are always detected regardless of this setting.

    [FastClonerClonable]
    [FastClonerPreserveIdentity]
    public class Document
    {
        public User Author { get; set; }
        public User LastEditor { get; set; }
    }
    
    // Or partial application
    [FastClonerClonable]
    public class Container
    {
        [FastClonerPreserveIdentity]
        public List<Node> Nodes { get; set; }
        
        public List<Item> Items { get; set; }
    }
  12. Handle safe handles with [FastClonerSafeHandle]

    next

    Use [FastClonerSafeHandle] on structs that act as handles to internal state or singletons. This instructs FastCloner to shallow-copy the readonly fields instead of deep-cloning them, preserving the original internal references and preventing breakage of internal framework logic.

    [FastClonerSafeHandle]
    public struct MyHandle
    {
        private readonly object _internalState; // Preserved (shared), not deep cloned
        public int Value; // Cloned normally
    }