Matthew Strawbridge
Matthew Strawbridge

Reputation: 20640

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

Upvotes: 4

Views: 379

Answers (2)

Gael Fraiteur
Gael Fraiteur

Reputation: 6857

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

}

Upvotes: 4

jlvaquero
jlvaquero

Reputation: 8785

I'm afraid this can not be done with PostSharp. PostSharp "injects" aspects code in your clases but you have to code the aspects. The key here is indetify common behavior and cross cutting concern in your system and model it as Aspects.

In the example of IIdentifiable you can see how GUID is a unique identifier that can be use by a lot of different classes in your system. It is common code, it is cross cutting concern and you find yourself REPEATING code in all your class entities so Identificable can be modeled as Aspect and get rid of repeating code.

As diferent classes has diferent Equals implementation you can not "deatach" (convert to aspect) the implementation of Equals. Equals is not a common behavior. Equals is not cross cutting concern. Equals can not be an Aspect (without reflection).

Upvotes: 1

Related Questions