📜 ⬆️ ⬇️

Attributes system

This article will focus on the unification of working with attributes in projects written in C #. The article is intended for developers of medium and large projects, or those who are interested in the subject of system design. All examples and implementations are conditional and are intended to reflect approaches or ideas.

Introduction


With the growth of the project, the number of different types of attributes grows. In each case, the developer chooses a convenient treatment option. With a large number of developers, approaches can vary widely, which complicates the understanding of systems. Typical examples of using attributes are: various types of serialization, cloning, binding, and the like.
To begin, consider the various options for using attributes, write down their pros and cons, and then try to get rid of the minuses by implementing a kind of generalized system.

Typical examples


Binding


Suppose we have a certain description of the user interface (UI, Form, Form) in an external source. There is a base class of the form Form, which allows you to load a description and create all the necessary controls (widgets, controls, widgets). Form users inherit from the Form class and mark up the required fields using attributes. When the Initialise method is called, the Form class, the created widgets are bound to the fields of the inheritor class.
Source
Widget class description:
using System; public class Widget { } 

Attribute class description:
 using System; [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class WidgetNameAttribute : Attribute { public WidgetNameAttribute(string name) { this.name = name; } public string Name { get { return name; } } private string name; } 

Form class description:
 using System; using System.Reflection; public class Form { public void Initialise() { FieldInfo[] fields = GetType().GetFields( BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { foreach (WidgetNameAttribute item in field.GetCustomAttributes( typeof(WidgetNameAttribute), false)) { Widget widget = FindWidget(item.Name); field.SetValue(this, widget); break; } } } public Widget FindWidget(string name) { return new Widget(); } } 

Custom form class description:
 using System; public class TestForm1 : Form { [WidgetName("Test1")] public Widget TestWidget1; [WidgetName("Test2")] public Widget TestWidget2; } 

Loading and form initialization:
 using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { TestForm1 form1 = new TestForm1(); form1.Initialise(); } } } 


Serialization


Suppose we have some description of the data in the xml file. It can be both data and, for example, settings. There is a base class Data, which allows you to search for values ​​and convert them to the required type. The user is inherited from the Data class and marks the fields with attributes, where XPath indicates the path to a specific field value in xml. When the Data class's InitialiseByXml method is called, the value is searched for and converted to the required type. In the current example, for simplicity, the converter is built into the class and supports only a few types.
Source
Attribute class description:
 using System; [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class DataValueAttribute : Attribute { public DataValueAttribute(string xPath) { this.xPath = xPath; } public string XPath { get { return xPath; } } private string xPath; } 

Data class description:
 using System; using System.Xml; using System.Reflection; using System.Collections.Generic; public class DataObject { static DataObject() { parsers[typeof(int)] = delegate(string value) { return int.Parse(value); }; parsers[typeof(string)] = delegate(string value) { return value; }; } public void InitialiseByXml(XmlNode node) { FieldInfo[] fields = GetType().GetFields( BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { foreach (DataValueAttribute item in field.GetCustomAttributes( typeof(DataValueAttribute), false)) { XmlNode tergetNode = node.SelectSingleNode(item.XPath); object value = parsers[field.FieldType](tergetNode.InnerText); field.SetValue(this, value); break; } } } private delegate object ParseHandle(string value); private static Dictionary<Type, ParseHandle> parsers = new Dictionary<Type, ParseHandle>(); } 

Custom class description:
 using System; public class TestDataObject1 : DataObject { [DataValue("Root/IntValue")] public int Value1; [DataValue("Root/StringValue")] public string Value2; } 

Usage example:
 using System; using System.Xml; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { XmlDocument doc = new XmlDocument(); doc.LoadXml( "<Root>" + "<IntValue>42</IntValue>" + "<StringValue>Douglas Adams</StringValue>" + "</Root>"); TestDataObject1 test = new TestDataObject1(); test.InitialiseByXml(doc); } } } 


Cloning


Suppose we have a class CloneableObject that allows you to clone yourself and all of your heirs. Cloning can be both normal and deep. The user inherits from CloneableObject and marks the fields that need to be cloned, and also indicates whether to use deep field cloning.
Source
Attribute class description:
 using System; [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class CloneAttribute : Attribute { public bool Deep { get { return deep; } set { deep = value; } } private bool deep; } 

CloneableObject class description:
 using System; using System.Reflection; public class CloneableObject : ICloneable { public object Clone() { object clone = Activator.CreateInstance(GetType()); FieldInfo[] fields = GetType().GetFields( BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { foreach (CloneAttribute item in field.GetCustomAttributes( typeof(CloneAttribute), false)) { object value = field.GetValue(this); if (item.Deep) { if (field.FieldType.IsArray) { Array oldArray = (Array)value; Array newArray = (Array)oldArray.Clone(); for (int index = 0; index < oldArray.Length; index++) newArray.SetValue(((ICloneable)oldArray.GetValue(index)). Clone(), index); value = newArray; } else { value = ((ICloneable)value).Clone(); } } field.SetValue(clone, value); break; } } return clone; } } 

Custom class description:
 using System; public class TestCloneableObject1 : CloneableObject { [Clone] public CloneableObject Value1; [Clone] public CloneableObject[] ValueArray2; [Clone(Deep = true)] public CloneableObject Value3; [Clone(Deep = true)] public CloneableObject[] ValueArray4; } 

Usage example:
 using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { TestCloneableObject1 test = new TestCloneableObject1(); test.Value1 = new CloneableObject(); test.ValueArray2 = new CloneableObject[] { new CloneableObject() }; test.Value3 = new CloneableObject(); test.ValueArray4 = new CloneableObject[] { new CloneableObject() }; TestCloneableObject1 test2 = (TestCloneableObject1)test.Clone(); } } } 


All examples have similar features and show the classic use of attributes.
Pros:

Minuses:

Unification


The main idea of ​​unification is the generation of initializers for a specific type of object and a specific set of attributes. To initialize an object, we request the necessary initializer and pass an object instance and data to it, the initializer does the rest. The initializer can be generated by code generation, or it can be passed on to the compiler using anonymous delegates.

Test class:
 using System; public class TestObject1 { [Clone, DataValue("Root/IntValue")] public int Value1; [DataValue("Root/StringValue")] public string Value2; [Clone, WidgetName("Test1")] public Widget Widget3; } 

A container will be created for the test class, inside which there are 3 initializers:

Test class and container created with initializers:
')


Implementation


The current implementation is a demonstration, does not adhere to any principles, does not handle errors and is not thread-safe. The main task of the implementation is to demonstrate the operation of the system.

Base Attribute Class


The delegates for the field are generated within attributes. All custom attributes are inherited from the FieldSetterAttribute attribute and override the Generate method, where the delegate is generated and added to the initializer list.
Source
 using System; using System.Reflection; [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] public abstract class FieldSetterAttribute : Attribute { public abstract void Generate(Initialiser initialiser, FieldInfo info); } 


Initializer


The task of the initializer is to keep in itself a list of delegates of a specific set of initialization and execute it on demand.
Source
 using System; using System.Collections.Generic; public delegate void InitialiseHandle(object target, object data); public class Initialiser { public void Add(InitialiseHandle handle) { handlers.Add(handle); } public void Initialise(object target, object data) { foreach (InitialiseHandle handler in handlers) handler(target, data); } private List<InitialiseHandle> handlers = new List<InitialiseHandle>(); } 


Container


The container contains all initializers for the same user-defined class type. Access to the initializer occurs by the type of attribute.
Source
 using System; using System.Collections.Generic; public class Container { public Initialiser GetInitialiser(Type type) { Initialiser result; if (!initialisers.TryGetValue(type, out result)) { result = new Initialiser(); initialisers.Add(type, result); } return result; } private Dictionary<Type, Initialiser> initialisers = new Dictionary<Type, Initialiser>(); } 


Type manager


The type manager contains the created containers for the custom types being processed. The generation of containers occurs upon request and is performed only once.
Source
 using System; using System.Collections.Generic; using System.Reflection; public static class TypeManager { public static Container GetContainer(Type type) { Container result; if (!containers.TryGetValue(type, out result)) { result = new Container(); containers.Add(type, result); InitialiseContainer(type, result); } return result; } private static void InitialiseContainer(Type type, Container container) { FieldInfo[] fields = type.GetFields( BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { foreach (FieldSetterAttribute item in field.GetCustomAttributes( typeof(FieldSetterAttribute), false)) { Initialiser initialiser = container.GetInitialiser(item.GetType()); item.Generate(initialiser, field); } } } private static Dictionary<Type, Container> containers = new Dictionary<Type, Container>(); } 


Principle of operation


The system user creates his attribute, inheriting it from the FieldSetterAttribute and overrides the Generate method. The Initialiser initializer associated with the user attribute type and fieldInfo information is passed to the Generate method. Inside the method, the user must create an anonymous delegate that performs the necessary operations on the field and adds it to the initializer.
When a container is requested for a user type, it is searched and, if it is not found, the generation of the container and all initializers for the user type is started. To do this, all fields are bypassed and if they have an attribute derived from FieldSetterAttribute, then the Generate method is called. After that, information about initializers is stored in the container, and the container is stored in the manager.
To use an initializer, it is necessary to query it by attribute type from the container and call the Initialise method, passing an instance of the class and the necessary data to it. After that, all delegates created for this initializer will be called.

Example of custom attribute implementation


After understanding the principles of the system, we can implement custom attributes for our test type TestObject1. The new test type is called TestObject2.
Source
Test class implementation:
 using System; public class TestObject2 : Form, ICloneable { [Clone, DataValue("Root/IntValue")] public int Value1; [DataValue("Root/StringValue")] public string Value2; [Clone(Deep = true), WidgetName("Test1")] public Widget Widget3; public object Clone() { object clone = Activator.CreateInstance(GetType()); TypeManager.GetContainer(GetType()). GetInitialiser(typeof(CloneAttribute)).Initialise(clone, this); return clone; } } 

Implementing a cloning-enabled widget:
 using System; public class Widget : ICloneable { public object Clone() { return new Widget(); } } 

Form implementation:
 using System; public class Form { public void Initialise() { TypeManager.GetContainer(GetType()). GetInitialiser(typeof(WidgetNameAttribute)).Initialise(this, null); } public Widget FindWidget(string name) { return new Widget(); } } 

The implementation of the attribute to bind:
 using System; using System.Reflection; [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class WidgetNameAttribute : FieldSetterAttribute { public WidgetNameAttribute(string name) { this.name = name; } public override void Generate(Initialiser initialiser, FieldInfo info) { initialiser.Add( delegate(object target, object data) { Form targetForm = (Form)target; Widget widget = targetForm.FindWidget(name); info.SetValue(targetForm, widget); } ); } private string name; } 

The implementation of the attribute for serialization:
 using System; using System.Reflection; using System.Xml; using System.Collections.Generic; [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class DataValueAttribute : FieldSetterAttribute { static DataValueAttribute() { parsers[typeof(int)] = delegate(string value) { return int.Parse(value); }; parsers[typeof(string)] = delegate(string value) { return value; }; } public DataValueAttribute(string xPath) { this.xPath = xPath; } public override void Generate(Initialiser initialiser, FieldInfo info) { ParseHandle parser = parsers[info.FieldType]; initialiser.Add( delegate(object target, object data) { XmlNode node = ((XmlNode)data).SelectSingleNode(xPath); object value = parser(node.InnerText); info.SetValue(target, value); } ); } private string xPath; private delegate object ParseHandle(string value); private static Dictionary<Type, ParseHandle> parsers = new Dictionary<Type, ParseHandle>(); } 

Implementing an attribute for cloning:
 using System; using System.Reflection; [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] public class CloneAttribute : FieldSetterAttribute { public override void Generate(Initialiser initialiser, FieldInfo info) { if (deep) { if (info.FieldType.IsArray) { initialiser.Add( delegate(object target, object data) { object value = info.GetValue(data); Array oldArray = (Array)value; Array newArray = (Array)oldArray.Clone(); for (int index = 0; index < oldArray.Length; index++) newArray.SetValue(((ICloneable)oldArray.GetValue(index)). Clone(), index); value = newArray; info.SetValue(target, value); } ); } else { initialiser.Add( delegate(object target, object data) { object value = info.GetValue(data); value = ((ICloneable)value).Clone(); info.SetValue(target, value); } ); } } else { initialiser.Add( delegate(object target, object data) { object value = info.GetValue(data); info.SetValue(target, value); } ); } } public bool Deep { get { return deep; } set { deep = value; } } private bool deep; } 


Total


As you can see from the examples, initializers can be called both inside the classes and outside. Complex assignments can be broken down into simple ones, since we know all the field parameters before we create the delegate. The request for fields and attributes occurs once. All work with attributes is monotonous.
The system can be extended to support properties, methods, events, and classes.

Source: https://habr.com/ru/post/168477/


All Articles