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:
static
list of properties.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
.
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.