Dynamic Aspect Oriented

A story about a specific subject model, where many valid field values ​​depend on the values ​​of others.



Task



It is easier to disassemble using a specific example: it is necessary to configure sensors with many parameters, but the parameters depend on each other. For example, the response threshold depends on the type of sensor, model and sensitivity, and possible models depend on the type of sensor, etc.



In our example, we take only the type of sensor and its value (the threshold at which it should be triggered).



public class Sensor { // Voltage, Temperature public SensorType Type { get; internal set; } //-400..400 for Voltage, 200..600 for Temperature public decimal Value { get; internal set; } }
      
      





Make sure that for voltage and temperature sensors the values ​​can only be in the ranges -400..400 and 200..600, respectively. All changes can be tracked and logged.



"A simple solution



The simplest implementation to maintain data consistency is to manually set restrictions and dependencies in setters and getters:



 public class Sensor { private SensorType _type; private decimal _value; public SensorType Type { get { return _type; } set { _type = value; if (value == SensorType.Temperature) Value = 273; if (value == SensorType.Voltage) Value = 0; } } public decimal Value { get { return _value; } set { if (Type == SensorType.Temperature && value >= 200 && value <= 600 || Type == SensorType.Voltage && value >= -400 && value <= 400) _value = value; } } }
      
      





One dependency generates a large amount of code that is difficult to read. Changing conditions or adding new dependencies is difficult to produce.



image



In a real project, such objects we had more than 30 dependent fields and more than 200 rules for each. The described solution, although working, would bring a huge headache in the development and support of such a system.



"Ideal" but unreal



Rules are easily described in short forms and can be placed next to the fields to which they relate. Perfectly:



 public class Sensor { public SensorType Type { get; set; } [Number(Type = SensorType.Temperature, Min = 200, Max = 600, Force = 273)] [Number(Type = SensorType.Voltage, Min = -400, Max = 400, Force = 0)] public decimal Value { get; set; } }
      
      





Force is what value to set if the condition changes.



Only the C # syntax will not allow writing this in attributes, since the list of fields on which the target property depends is not predefined.



Working approach



We will write the rules as follows:



 public class Sensor { public SensorType Type { get; set; } [Number("Type=Temperature", "200..600", Force = "273")] [Number("Type=Voltage", "-400..400", Force = "0")] public decimal Value { get; set; } }
      
      





It remains to make it work. Such a class is simply useless.



Dispatcher



The idea is simple - close the setters and change the field values ​​through a certain dispatcher, who will understand all the rules, monitor their execution, notify about field changes and log all changes.



image



The option is working, but the code will look awful:



 someDispatcher.Set(mySensor, "Type", SensorType.Voltage);
      
      





You can of course make the dispatcher an integral part of objects with dependencies:



 mySensor.Set("Type", SensorType.Voltage)
      
      





But my objects will be used by other developers, and sometimes it will not be completely clear to them why it is so necessary to write. After all, I want to write simply:



 mySensor.Type=SensorType.Voltage;
      
      





Inheritance



Specifically, in our model, we ourselves managed the life cycle of objects with dependencies - we created them only in the model itself and outwardly provided only their editing. Therefore, we will make all the fields virtual, we will leave the external interface of the model unchanged, but it will work with the classes of “wrappers”, which will implement the logic of checks.



image



This is an ideal option for an external user, he will work with such an object in the usual way



 mySensor.Type=SensorType.Voltage
      
      





It remains to learn how to create such wrappers



Class Generation



There are actually two ways to generate:





Generating code based on attributes is definitely cool and will work quickly. But how much it will require strength. And, most importantly, if you need to add new restrictions / rules, how many changes will be required and what complexity?



We will generate standard code for each setter, which will call methods that will analyze the attributes and perform checks.



We will leave getter unchanged:



 MethodBuilder getPropMthdBldr = typeBuilder.DefineMethod("get_" + property.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, property.PropertyType, Type.EmptyTypes); ILGenerator getIl = getPropMthdBldr.GetILGenerator(); getIl.Emit(OpCodes.Ldarg_0); getIl.Emit(OpCodes.Call, property.GetMethod); getIl.Emit(OpCodes.Ret);
      
      





Here, the first parameter that came to our method is put on the stack, this is a reference to the object (this). Then the getter of the base class is called and the result is returned, which is put on the top of the stack. Those. our getter simply forwards the call to the base class.



Setter is a bit trickier. For analysis, we will create a static method, which will produce the analysis in approximately the following way:



 if (StrongValidate(this, property, value)) { value = SoftValidate(this, property, value); if (oldValue != value) { <    value>; ForceValidate(baseModel, property); Log(baseModel, property, value, oldValue); } }
      
      





StrongValidate - will discard values ​​that cannot be converted to those that fit the rules. For example, only “y” and “n” are allowed in the text box; when you try to write "u", you just have to reject the changes so that the model is not destroyed.



  [String("", "y, n")]
      
      





SoftValidate - will convert values ​​from inappropriate to valid. For example, an int field can only accept numbers. When you try to write 111, you can convert the value to the nearest suitable one - "9".



  [Number("", "0..9")]
      
      





<call the base setter with value> - after we get a valid value, you need to call the setter of the base class to change the value of the field.



ForceValidate - after the change, we can get an invalid model in those fields that depend on our field. For example, changing Type causes a change in Value.



Log is just notification and logging.



To call such a method, we need the object itself, its new and old value, and the field that is changing. The code for such a setter will look like this:



 MethodBuilder setPropMthdBldr = typeBuilder.DefineMethod("set_" + property.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, null, new[] { property.PropertyType }); //  ,    var setter = typeof(DinamicWrapper).GetMethod("Setter", BindingFlags.Static | BindingFlags.Public); ILGenerator setIl = setPropMthdBldr.GetILGenerator(); setIl.Emit(OpCodes.Ldarg_0);//     - this setIl.Emit(OpCodes.Ldarg_1);//     -  setter    (value) if (property.PropertyType.IsValueType) //  Boxing,    Value,         object { setIl.Emit(OpCodes.Box, property.PropertyType); } setIl.Emit(OpCodes.Ldstr, property.Name); //     setIl.Emit(OpCodes.Call, setter); setIl.Emit(OpCodes.Ret);
      
      





We will need another method that will directly change the value of the base class. The code is similar to a simple getter, only there are two parameters - this and value:



 MethodBuilder setPureMthdBldr = typeBuilder.DefineMethod("set_Pure_" + property.Name, MethodAttributes.Public, CallingConventions.Standard, null, new[] { property.PropertyType }); ILGenerator setPureIl = setPureMthdBldr.GetILGenerator(); setPureIl.Emit(OpCodes.Ldarg_0); setPureIl.Emit(OpCodes.Ldarg_1); setPureIl.Emit(OpCodes.Call, property.GetSetMethod()); setPureIl.Emit(OpCodes.Ret);
      
      





All code with small tests can be found here:

github.com/wolf-off/DinamicAspect



Validations



The codes of the validations themselves are simple - they just look for the current active attribute according to the principle of the longest condition and ask him if the new value is valid. You only need to consider two things when choosing rules (parsing them and calculating the appropriate ones):





Conclusion



What is the advantage of this approach?



And that after creating the object:



 var target = DinamicWrapper.Create<Sensor>();
      
      





It can be used as usual, but it will behave according to the attributes:



 target.Type = SensorType.Temperature; target.Value=300; Assert.AreEqual(target.Value, 300); // true target.Value=3; Assert.AreEqual(target.Value, 200); // true - minimum target.Value=3000; Assert.AreEqual(target.Value, 600); // true - maximum target.Type = SensorType.Voltage; Assert.AreEqual(target.Value, 0); // true - minimum target.Value= 3000; Assert.AreEqual(target.Value, 400); // true - maximum
      
      






All Articles