Search code examples
c#.netlambdareflectionexpression

Assignment and comparison of class properties without boxing


Consider the following scenario:

public interface ICloneable<T>:
    ICopyable<T> where T : ICloneable<T>, new()
{
    public T CloneTyped ();
}

public interface ICopyable<T>
    where T : ICopyable<T>
{
    public T CopyTo (T other);
    public T CopyFrom (T other);
}

public interface IDirtyable<T>
    where T : IDirtyable<T>
{
    // Compares the current object [this] to an unaltered object [referenceObject].
    public bool IsDirty (T referenceObject);
}

public partial class Model:
    ICloneable<Model>,
    ICopyable<Model>,
    IDirtyable<Model>
{
    public virtual long Id { get; set; }
    public virtual DateTime DateTimeCreated { get; set; }
    public virtual DateTime DateTimeModified { get; set; }
    public virtual string Name { get; set; } = string.Empty;

    public Model CloneTyped () => new Model().CopyFrom(this);
    public Model CopyFrom (Model other) => other.CopyTo(this);

    public Model CopyTo (Model other)
    {
        other.Id = this.Id;
        other.DateTimeCreated = this.DateTimeCreated;
        other.DateTimeModified = this.DateTimeModified;
        other.Name = this.Name;

        return (other);
    }

    public bool IsDirty (Model referenceObject)
    {
        return
            this.Id == referenceObject.Id
            && this.DateTimeCreated == referenceObject.DateTimeCreated
            && this.DateTimeModified == referenceObject.DateTimeModified
            && this.Name == referenceObject.Name;
    }
}

Please note that for the sake of this question, I am not interested in INotifyPropertyChanged or other similar mechanisms. Furthermore, assume that the class Model has dozens of scalar properties (we do not care about composite types).

So we are left with an interface pattern that performs a comparison or assignment operation on two objects of the same type. Now, since I know the list of properties that should be processed by these interface implementations, I could do something like the following:

    public bool IsDirtyUsingReflection (Model referenceObject)
    {
        var equal = true;
        var propertyNames = new [] { nameof(this.Id), nameof(this.DateTimeCreated), nameof(this.DateTimeModified), nameof(this.Name), };
        var properties = this.GetType().GetProperties().Where(p => propertyNames.Contains(p.Name));

        foreach (var property in properties)
        {
            equal
                &= property.GetValue(this)
                ?.Equals(property.GetValue(referenceObject))
                ?? false;

            if (!equal) { return (true); }
        }

        return (false);
    }

So, the question is, how could I maintain a list of properties and compare/assign them without reflection or boxing?

I thought about maintaining such properties as Expression types.

var properties = new Expression<Func<T, object?>> [] {...};

Any guidance would be appreciated to achieve the following:

  • Maintain a static list of properties.
  • Perform assignment and comparison operations without boxing.
  • Perform assignment and comparison operations without reflection.
  • Maintain a list of Expression objects or compiled lambdas that avoid boxing. Compilation and/or boxing at a type-level (static1) is fine.

The data types to be considered only include built-in value types, string, enums, and structs that implement IEquitable.


Solution

  • Expression generation or raw ref-emit (ILGenerator) are indeed viable, however: I wonder whether a better approach here would be to write a Roslyn code generator to do this for you. For example, you could write:

    public partial class Model
    {
        public partial bool IsDirty (Model referenceObject);
    }
    

    and the generator could detect that and output the missing half based on the members discovered during build:

    partial class Model
    {
        public partial bool IsDirty (Model referenceObject)
        {
            return
                this.Id == referenceObject.Id
                && this.DateTimeCreated == referenceObject.DateTimeCreated
                && this.DateTimeModified == referenceObject.DateTimeModified
                && this.Name == referenceObject.Name;
        }
    }
    

    A bit more work, perhaps, but it can be more flexible and performant in the long run - it is very hard to maintain expression / ref-emit code (ask me how I know...), where-as making tweaks to generator output: simple.