Una historia sobre un modelo de tema específico, donde muchos valores de campo válidos dependen de los valores de otros.
Desafío
Es más fácil desmontar usando un ejemplo específico: es necesario configurar sensores con muchos parámetros, pero los parámetros dependen unos de otros. Por ejemplo, el umbral de respuesta depende del tipo de sensor, modelo y sensibilidad, y los posibles modelos dependen del tipo de sensor, etc.
En nuestro ejemplo, tomamos solo el tipo de sensor y su valor (el umbral en el que debe activarse).
public class Sensor {
Asegúrese de que para los sensores de voltaje y temperatura los valores solo pueden estar en los rangos -400..400 y 200..600, respectivamente. Todos los cambios pueden ser rastreados y registrados.
La solución "simple"
La implementación más fácil para mantener la consistencia de los datos es establecer manualmente restricciones y dependencias en los establecedores y captadores:
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; } } }
Una dependencia genera una gran cantidad de código que es difícil de leer. Cambiar las condiciones o agregar nuevas dependencias es difícil.

En un proyecto real, dichos objetos tenían más de 30 campos dependientes y más de 200 reglas para cada uno. La solución descrita, aunque funciona, traería un gran dolor de cabeza en el desarrollo y soporte de dicho sistema.
"Ideal" pero irreal
Las reglas se describen fácilmente en formas cortas y se pueden colocar al lado de los campos con los que se relacionan. Idealmente:
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; } }
La fuerza es qué valor establecer si la condición cambia.
Solo la sintaxis de C # no permitirá escribir esto en los atributos, ya que la lista de campos de los que depende la propiedad de destino no está predefinida.
Enfoque de trabajo
Escribiremos las reglas de la siguiente manera:
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; } }
Queda por hacer que funcione. Tal clase es simplemente inútil.
Despachador
La idea es simple: cierre los establecedores y cambie los valores de campo a través de un despachador determinado, que comprenderá todas las reglas, supervisará su ejecución, notificará sobre los cambios de campo y registrará todos los cambios.

La opción está funcionando, pero el código se verá horrible:
someDispatcher.Set(mySensor, "Type", SensorType.Voltage);
Por supuesto, puede hacer que el despachador sea una parte integral de los objetos con dependencias:
mySensor.Set("Type", SensorType.Voltage)
Pero mis objetos serán utilizados por otros desarrolladores y, a veces, no les quedará completamente claro
por qué es tan necesario escribir. Después de todo, quiero escribir simplemente:
mySensor.Type=SensorType.Voltage;
Herencia
Específicamente, en nuestro modelo, nosotros mismos gestionamos el ciclo de vida de los objetos con dependencias: los creamos solo en el modelo mismo y externamente solo proporcionamos su edición. Por lo tanto, haremos que todos los campos sean virtuales, dejaremos la interfaz externa del modelo sin cambios, pero ya funcionará con clases de "envoltorios", que implementarán la lógica de las comprobaciones.

Esta es una opción ideal para un usuario externo, trabajará con dicho objeto de la manera habitual
mySensor.Type=SensorType.Voltage
Queda por aprender cómo crear tales envoltorios
Generación de clase
En realidad, hay dos formas de generar:
- Generar código completo basado en atributos
- Genere código completo que llame a la verificación de atributos
Generar código basado en atributos es definitivamente genial y funcionará rápidamente. Pero cuánto requerirá fuerza. Y, lo más importante, si necesita agregar nuevas restricciones / reglas, ¿cuántos cambios serán necesarios y qué complejidad?
Generaremos código estándar para cada setter, que llamará a métodos que analizarán los atributos y realizarán verificaciones.
Vamos a dejar getter sin cambios:
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);
Aquí, el primer parámetro que llegó a nuestro método se inserta en la pila, esto es una referencia al objeto (esto). Luego se llama al captador de la clase base y se devuelve el resultado, que se coloca en la parte superior de la pila. Es decir nuestro captador simplemente reenvía la llamada a la clase base.
Setter es un poco más complicado. Para el análisis, crearemos un método estático, que producirá el análisis aproximadamente de la siguiente manera:
if (StrongValidate(this, property, value)) { value = SoftValidate(this, property, value); if (oldValue != value) { < value>; ForceValidate(baseModel, property); Log(baseModel, property, value, oldValue); } }
StrongValidate: descartará valores que no se puedan convertir a aquellos que se ajusten a las reglas. Por ejemplo, solo se permite escribir "y" y "n" en el cuadro de texto; cuando intenta escribir "u", solo tiene que rechazar los cambios para que el modelo no se destruya.
[String("", "y, n")]
SoftValidate: convertirá valores de inapropiados a válidos. Por ejemplo, un campo int solo puede aceptar números. Cuando intente escribir 111, puede convertir el valor al más adecuado: "9".
[Number("", "0..9")]
<llamar al establecedor base con valor>: después de obtener un valor válido, debe llamar al establecedor de la clase base para cambiar el valor del campo.
ForceValidate: después del cambio, podemos obtener un modelo no válido en los campos que dependen de nuestro campo. Por ejemplo, cambiar el tipo provoca un cambio en el valor.
El registro es solo notificación y registro.
Para llamar a tal método, necesitamos el objeto en sí, su valor nuevo y antiguo, y el campo que está cambiando. El código para tal setter se verá así:
MethodBuilder setPropMthdBldr = typeBuilder.DefineMethod("set_" + property.Name, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Virtual, null, new[] { property.PropertyType });
Necesitaremos otro método que cambie directamente el valor de la clase base. El código es similar a un getter simple, solo hay dos parámetros: esto y valor:
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);
Todo el código con pequeñas pruebas se puede encontrar aquí:
github.com/wolf-off/DinamicAspectValidaciones
Los códigos de las validaciones en sí son simples: solo buscan el atributo activo actual de acuerdo con el principio de la condición más larga y le preguntan si el nuevo valor es válido. Solo debe tener en cuenta dos cosas al elegir las reglas (analizarlas y calcular las apropiadas):
- Caché el resultado de GetCustomAttributes. Una función que toma atributos de los campos funciona lentamente porque los crea cada vez. Caché su resultado. Implementé en la clase base BaseModel
- Al calcular las reglas apropiadas, tendrá que lidiar con los tipos de campo. Si todos los valores se reducen a cadenas y se comparan, funcionará lentamente. Especialmente enum. Implementado en la clase de atributo base DependencyAttribute
Conclusión
¿Cuál es la ventaja de este enfoque?
Y eso después de crear el objeto:
var target = DinamicWrapper.Create<Sensor>();
Se puede usar como de costumbre, pero se comportará de acuerdo con los atributos:
target.Type = SensorType.Temperature; target.Value=300; Assert.AreEqual(target.Value, 300);