Search code examples
c#pocoinotifypropertychangedcilreflection.emit

IL Emit - set an existing property with a boolean value before notifypropertychanged


I'm implementing an emitted propertychanged handler for my POCO object containing virtual auto-properties, and I've got code that works to the point where propertychanged is being raised whenever I change the underlying property. The reason for doing this is that I am sharing a POCO object with the server (for better or for worse), where I will be sending modified objects to the server. I cannot decorate the POCO object with attributes (since the server would also have these decorators, as we share the common class) and I cannot use third-party tools such as Fody or PostSharp due to policies. I need to track whether the object has been modified, and I'm stuck on this.

Here is the Emit that wraps my virtual auto-properties with change notification:

    MethodBuilder setMethodBuilder = typeBuilder.DefineMethod(setMethod.Name, setMethod.Attributes, setMethod.ReturnType, types.ToArray());
    typeBuilder.DefineMethodOverride(setMethodBuilder, setMethod);
    ILGenerator wrapper = setMethodBuilder.GetILGenerator();

    ...Emit if property <> value IsModified=true here...

    wrapper.Emit(OpCodes.Ldarg_0);
    wrapper.Emit(OpCodes.Ldarg_1);
    wrapper.EmitCall(OpCodes.Call, setMethod, null);

What I need to do is get the set method of the existing "IsModified" boolean property and set it if the property value <> value.

Here's an example of what I'd like to emit (this is currently defined as a POCO with virtual auto-properties):

public class AnEntity
{
    string _myData;
    public string MyData
    {
        get
        {
            return _myData;
        }
        set
        {
            if(_myData <> value) 
            {
                IsModified = true;
                _myData = value;
                OnPropertyChanged("MyData");                
            }
        }
    }

    bool _isModified;
    public bool IsModified { get; set; }
    {
        get
        {
            return _isModified;
        }
        set
        {
            _isModified = value;
            OnPropertyChanged("IsModified");
        }
    }
}

I've been stuck on this for a while...I have managed to create a new property called "NewIsModified" in the new proxy class that was created, however, I'd very much like to reuse the existing IsModified property in my original POCO.

I hope I've explained my question properly and is easy to understand. Any help would be greatly appreciated, and I hope it will help someone else, too.

Kind regards.


Solution

  • Here is a working code for do it in Mono.Cecil

    C# code before:

    public class AnEntityVirtual
    {
        public virtual string MyData { get; set; }
        public virtual bool IsModified { get; set; }
    }
    

    IL code of the set_MyData before:

    IL_0000: ldarg.0
    IL_0001: ldarg.1
    IL_0002: stfld string ClassLibrary1.AnEntityVirtual::'<MyData>k__BackingField'
    IL_0007: ret
    

    The rewriting:

    // Read the module and get the relevant type
    var assemblyPath = $"{Environment.CurrentDirectory}\\ClassLibrary1.dll";
    var module = ModuleDefinition.ReadModule(assemblyPath);
    var type = module.Types.Single(t => t.Name == "AnEntityVirtual");
    
    // Get the method to rewrite
    var myDataProperty = type.Properties.Single(prop => prop.Name == "MyData");
    var isModifiedSetMethod = type.Properties.Single(prop => prop.Name == "IsModified").SetMethod;
    var setMethodBody = myDataProperty.SetMethod.Body;
    
    // Initilize before rewriting (clear pre instructions, create locals and init them)
    setMethodBody.Instructions.Clear();
    var localDef = new VariableDefinition(module.TypeSystem.Boolean);
    setMethodBody.Variables.Add(localDef);
    setMethodBody.InitLocals = true;
    
     // Get fields\methos to use in the new method body
     var propBackingField = type.Fields.Single(field => field.Name == $"<{myDataProperty.Name}>k__BackingField");
    var equalMethod =
                myDataProperty.PropertyType.Resolve().Methods.FirstOrDefault(method => method.Name == "Equals") ??
                module.ImportReference(typeof(object)).Resolve().Methods.Single(method => method.Name == "Equales");
    var equalMethodReference = module.ImportReference(equalMethod);
    
    // Start the rewriting
    var ilProcessor = setMethodBody.GetILProcessor();
    
    // First emit a Ret instruction. This is beacause we want a label to jump if the values are equals
    ilProcessor.Emit(OpCodes.Ret);
    var ret = setMethodBody.Instructions.First();
    
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_0)); // load 'this'
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldfld, propBackingField)); // load backing field
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_1)); // load 'value'
    ilProcessor.InsertBefore(ret, ilProcessor.Create(equalMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt, equalMethodReference)); // call equals
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Stloc_0)); // store result
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldloc_0)); // load result
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Brtrue_S, ret)); // check result and jump to Ret if are equals
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_0)); // load 'this'
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldc_I4_1)); // load 1 ('true')
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Call, isModifiedSetMethod)); // set IsModified to 'true'
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_0)); // load this
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Ldarg_1)); // load 'value'
    ilProcessor.InsertBefore(ret, ilProcessor.Create(OpCodes.Stfld, propBackingField)); // store 'value' in backing field
    // here you can call to Notify or whatever you want
    module.Write(assemblyPath.Replace(".dll", "_new") + ".dll"); // save the new assembly
    

    C# code after:

    public virtual string MyData
    {
        [CompilerGenerated]
        get
        {
            return this.<MyData>k__BackingField;
        }
        [CompilerGenerated]
        set
        {
            if (!this.<MyData>k__BackingField.Equals(value))
            {
                this.IsModified = true;
                this.<MyData>k__BackingField = value;
            }
        }
    }
    

    IL code after:

    IL_0000: ldarg.0
    IL_0001: ldfld string ClassLibrary1.AnEntityVirtual::'<MyData>k__BackingField'
    IL_0006: ldarg.1
    IL_0007: callvirt instance bool [mscorlib]System.String::Equals(object)
    IL_000c: stloc.0
    IL_000d: ldloc.0
    IL_000e: brtrue.s IL_001e
    
    IL_0010: ldarg.0
    IL_0011: ldc.i4.1
    IL_0012: call instance void ClassLibrary1.AnEntityVirtual::set_IsModified(bool)
    IL_0017: ldarg.0
    IL_0018: ldarg.1
    IL_0019: stfld string ClassLibrary1.AnEntityVirtual::'<MyData>k__BackingField'
    
    IL_001e: ret
    

    As I wrote, this is an example of how to do it in Cecil. In your real code, you can base on that, but with some changes.

    For example, you can create private field for your property and not use the compiler generated backing field.

    You may call OptimizeMacros.

    Also if you know exactly which property you need to rewrite, you can call to other equal method, e.g. if it's string, you can call a static method of type string op_Equality or op?_Inequality this is the == and != of string