Search code examples
c#aoppostsharp

Implement interfaces based on class properties without reflection


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;
    }
}

Solution

  • 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));
            }
        }
    
    }