views:

77

answers:

2

I'm revisiting my class tracking (dirty logic), which I wrote last year. Currently I have an uber base class that deals with all the state tracking, but each property whos values I need to track needs to stick to the standard get { return _x; } set { _isDirty = true; _x = value; } way of working.

After playing with Entity Framework and reading up on the Proxy Pattern, I was hoping there was a nicer way to implement my IsDIrty Logic whilst being able to make use of auto implemented properties?

To be completely honest, I haven't a clue of what I'm talking about. Is there a way I can do something like the following:

public class Customer : ITrackable
{
    [TrackState(true)] // My own attribute
    public virtual string Name { get;set;}

    [TrackState(true)]
    public virtual  int Age { get;set;}

    // From ITrackable
    public bool IsDirty { get; protected set; }

}

And then implement a dynamic proxy which will use reflection (or another magical solution) to call another method first before setting the values on the properties with the TrackState attribute.

Obviously I could easily do this by creating a phyiscal proxy class and use IoC:

public class CustomerProxy : Customer
{
    Customer _customer;

    public override string Name 
    {
        get { return _customer.Name; }
        set { IsDirty = true; return _customer.Name; }
    }

    // Other properties
}

But I don't fancy having to do this for every object, otherwise there's no benefit from my existing solution. Hope someone can satisfy my curiosity, or at least tell me how EF achieves it.

+1  A: 

Castle's DynamicProxy does exactly this: http://www.castleproject.org/dynamicproxy/index.html

Allows you to provide an interceptor:

public void Intercept(IInvocation invocation)
{
    // Call your other method first...  then proceed
    invocation.Proceed();
}

You get access to the MethodInfo object via invocation.Method. You may override the return value by setting invocation.ReturnValue. And you can access (and override) the arguments.

Kirk Woll
+1  A: 

PostSharp could help.

Or if you're feeling like it, you can write your own IL-rewriter for this. Mono.Cecil is a great library that'll make it a breeze. Here is quick concoction:

class Program {

  static ModuleDefinition _module;

  static void Main(string[] args) {
    // the argument is the assembly path
    _module = ModuleDefinition.ReadModule(args[0]);
    var trackables = _module.Types.
      Where(type => type.Interfaces.Any(tr => tr.Name == "ITrackable"));
    var properties = trackables.SelectMany(type => type.Properties);
    var trackableProperties = properties.
      Where(property => property.CustomAttributes.
        Any(ca => ca.Constructor.DeclaringType.Name == "TrackStateAttribute"));
    trackableProperties.
      Where(property => property.SetMethod != null).
      ToList().
      ForEach(property => CallIsDirty(property.SetMethod));
    _module.Write(args[0]);
  }

  private static void CallIsDirty(MethodDefinition setter) {
    Console.WriteLine(setter.Name);

    var isDirty = setter.DeclaringType.Methods.
      Single(method => method.Name == "set_IsDirty");
    var reference = new MethodReference(isDirty.Name,
      _module.Import(typeof(void))) {
        DeclaringType = setter.DeclaringType,  
        HasThis = true,
        CallingConvention = MethodCallingConvention.Default
      };
    reference.Parameters.Add(new ParameterDefinition(
      _module.Import(typeof(bool))));
    var IL = setter.Body.GetILProcessor();
    var param0 = IL.Create(OpCodes.Ldarg_0);
    var param1 = IL.Create(OpCodes.Ldc_I4_1);
    var call = IL.Create(OpCodes.Call, reference);
    IL.InsertBefore(setter.Body.Instructions[0], call);
    IL.InsertBefore(setter.Body.Instructions[0], param1);
    IL.InsertBefore(setter.Body.Instructions[0], param0);
  }
}

It uses these helpers:

public class TrackStateAttribute : Attribute { }

public interface ITrackable { bool IsDirty { get; } }

Example code:

public class Customer : ITrackable {
  [TrackState] public string Name { get; set; }
  [TrackState] public int Age { get; set; }
  public bool IsDirty { get; protected set; }
}

It's assumed that the IsDirty property will also have a setter.

Jordão
+1 Thanks! I already had `Castle.Core` in my project, so I went with DynamicProxy instead of this.
GenericTypeTea