This page on the PostSharp website has the following teaser:
One of the common situations that you will encounter is the need to implement a specific interface on a large number of classes. This may be
INotifyPropertyChanged
,IDispose
,IEquatable
or some custom interface that you have created.
I'd like to write a custom aspect that implements a general version of IEquatable
based on the properties of the class it's applied to (preferably at compile-time instead of by using reflection at runtime). It would be good to just be able to add an attribute to a simple class rather than having to implement a custom method each time. Is that possible? I'd hope so, since it's specifically called out in this introduction, but I haven't been able to track down any example code.
I've seen this example from the PostSharp website that includes an example of introducing the IIdentifiable
interface. But it just returns a GUID
that's independent of the class that the new interface is added to.
Is there a way to construct a custom attribute that implements IEquatable
based on the properties of the type that it's applied to (i.e. making two instances equal if all of their properties are equal)?
I've found a solution using T4 templates but would like to know if the same can be achieved using PostSharp.
Edit:
To be clear, I'd like to be able to write something like this:
[AutoEquatable]
public class Thing
{
int Id { get; set; }
string Description { get; get; }
}
and have it automatically converted to this:
public class Thing
{
int Id { get; set; }
string Description { get; get; }
public override bool Equals(object other)
{
Thing o = other as Thing;
if (o == null) return false;
// generated in a loop based on the properties
if (!Id.Equals(o.Id)) return false;
if (!Description.Equals(o.Description)) return false;
return true;
}
}
This is possible with PostSharp 4.0 using the following code;
[PSerializable]
class EquatableAttribute : InstanceLevelAspect, IAdviceProvider
{
public List<ILocationBinding> Fields;
[ImportMember("Equals", IsRequired = true, Order = ImportMemberOrder.BeforeIntroductions)]
public Func<object, bool> EqualsBaseMethod;
[IntroduceMember(IsVirtual = true, OverrideAction = MemberOverrideAction.OverrideOrFail)]
public new bool Equals(object other)
{
// TODO: Define a smarter way to determine if base.Equals should be invoked.
if (this.EqualsBaseMethod.Method.DeclaringType != typeof(object) )
{
if (!this.EqualsBaseMethod(other))
return false;
}
object instance = this.Instance;
foreach (ILocationBinding binding in this.Fields)
{
// The following code is inefficient because it boxes all fields. There is currently no workaround.
object thisFieldValue = binding.GetValue(ref instance, Arguments.Empty);
object otherFieldValue = binding.GetValue(ref other, Arguments.Empty);
if (!object.Equals(thisFieldValue, otherFieldValue))
return false;
}
return true;
}
// TODO: Implement GetHashCode the same way.
public IEnumerable<AdviceInstance> ProvideAdvices(object targetElement)
{
Type targetType = (Type) targetElement;
FieldInfo bindingField = this.GetType().GetField("Fields");
foreach (
FieldInfo field in
targetType.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic))
{
yield return new ImportLocationAdviceInstance(bindingField, new LocationInfo(field));
}
}
}