📜 ⬆️ ⬇️

Representation of metadata using enumerations

What is metadata? This is some descriptive information that is used by program algorithms. For example, these may be the names of tables and database fields, the names of registry keys in which the required parameters are stored, or the names of properties of objects accessed by the program. I want to tell you how using extension methods and attributes you can conveniently use enums to represent metadata.


Request generation task


Some time ago I was developing a plugin that implements some specific functionality for a popular document management system. In particular, it was necessary to display a list of documents that meet certain conditions. To generate a list of documents, it was necessary to form a request to the server in the embedded language (some subset of XPath). The query is built dynamically, depending on the set of parameters, and some parameters may not be specified. Therefore, the use of a static string with substitutions was unacceptable. How can you solve this problem? The easiest way is to use string values ​​that match the search attributes and conditions directly in the function that generates the query:

 string BuildQuery() { ... if (IsDateFromSpecified) strQuery += "DateFrom >= '" + DateFromValue.ToString() + "'"; ... } 

')
I think this option is quite applicable in some scenarios (no need to throw rotten apples at me). For example, if a simple utility is made that operates with two attributes that are mentioned once and only in one function, then it may be inappropriate to make a garden with metadata. But if the request consists of a dozen conditions, and even the requests are different, then the described approach, to put it mildly, is inappropriate. I would single out several reasons why I consider this approach unsuccessful:


Select constants


What to do? The first, most promising solution is to use constants for attribute names, and formatted strings to describe conditions. The result will look something like this:

 const string MORE_OR_EQUALS_EXPRESSION = "{0} >= '{1}'"; const string DATE_FROM_ATTRIBUTE = "DateFrom"; string BuildQuery() { ... if (IsDateFromSpecified) strQuery += string.Format(MORE_OR_EQUALS_EXPRESSION, DATE_FROM_ATTRIBUTE, DateFromValue.ToString()); ... } 
const string MORE_OR_EQUALS_EXPRESSION = "{0} >= '{1}'"; const string DATE_FROM_ATTRIBUTE = "DateFrom"; string BuildQuery() { ... if (IsDateFromSpecified) strQuery += string.Format(MORE_OR_EQUALS_EXPRESSION, DATE_FROM_ATTRIBUTE, DateFromValue.ToString()); ... }


Of course, the resulting code is easier to maintain, easier to make changes to it, but I can not say that it has become more readable. A little thought, I found a solution, which, in fact, I wanted to share. I will reveal the details below, but for now just a sample of the request generation code (from the production code):

 // -  predicate.Add(DGA.FlowId.EqualsTo(WorksFlowId)); //     predicate.Add(DGA.DogovorOfWorkID.EqualsTo(CurrentDocument.Id)); // if (DateFrom != null) predicate.Add(DGA.WorkEndsAt.MoreOrEqualThan(DateFrom.Value)); if (DateTo != null) predicate.Add(DGA.WorkStartsAt.LessOrEqualThan(DateTo.Value)); //   if (OnlyActive) predicate.Add(DGA.WorkState.EqualsTo(WorkState.Planned.ToString())); 
// - predicate.Add(DGA.FlowId.EqualsTo(WorksFlowId)); // predicate.Add(DGA.DogovorOfWorkID.EqualsTo(CurrentDocument.Id)); // if (DateFrom != null) predicate.Add(DGA.WorkEndsAt.MoreOrEqualThan(DateFrom.Value)); if (DateTo != null) predicate.Add(DGA.WorkStartsAt.LessOrEqualThan(DateTo.Value)); // if (OnlyActive) predicate.Add(DGA.WorkState.EqualsTo(WorkState.Planned.ToString()));


In my opinion, this code is quite understandable. Since all syntactic constructions of the query language are designed as methods, the syntax of the query is controlled at the compilation stage. The resulting code is easy to read and easy to change. And I managed to achieve such a code using enumerations, extensions and attributes.

Use enumeration


So, instead of string constants, I designed the search fields as an enumeration. The string name of the search attribute (it has a hard-to-read XPath-like form) is contained in the Description attribute:

 public enum DGA { [Description("doc_RegCard/rc_Index/date__")] RegDate, [Description("doc_RegCard/rc_Index/text__")] RegNum, [Description("doc_RegCard/rc_Index/text___")] Manager, [Description("doc_RegCard/rc_FlowKey")] FlowId, [Description("doc_RegCard/rc_Index/text__")] Title, [Description("doc_RegCard/rc_Index/text__")] Subject, ... } 
public enum DGA { [Description("doc_RegCard/rc_Index/date__")] RegDate, [Description("doc_RegCard/rc_Index/text__")] RegNum, [Description("doc_RegCard/rc_Index/text___")] Manager, [Description("doc_RegCard/rc_FlowKey")] FlowId, [Description("doc_RegCard/rc_Index/text__")] Title, [Description("doc_RegCard/rc_Index/text__")] Subject, ... }


Each elementary condition on an attribute is drawn up as an extension method for this enumeration. Since each line for the condition is used only once, I did not put them in separate constants, but use it directly in the function. Here is an example of some of these features:

 public static string LessOrEqualThan(this DGA attrib, DateTime val) { return attrib.LessOrEqualThan(System.Xml.XmlConvert .ToString(val, System.Xml.XmlDateTimeSerializationMode.Unspecified)); } public static string LessOrEqualThan(this DGA attrib, string val) { return String.Format("{0} <= '{1}'", attrib.GetAttribName(), val); } public static string InList(this DGA attrib, IEnumerable<string> values) { return String.Format("{0} in list({1})", attrib.GetAttribName(), values .Select(x => "'" + x + "'") .Aggregate((x, y) => x + ", " + y)); } 
public static string LessOrEqualThan(this DGA attrib, DateTime val) { return attrib.LessOrEqualThan(System.Xml.XmlConvert .ToString(val, System.Xml.XmlDateTimeSerializationMode.Unspecified)); } public static string LessOrEqualThan(this DGA attrib, string val) { return String.Format("{0} <= '{1}'", attrib.GetAttribName(), val); } public static string InList(this DGA attrib, IEnumerable<string> values) { return String.Format("{0} in list({1})", attrib.GetAttribName(), values .Select(x => "'" + x + "'") .Aggregate((x, y) => x + ", " + y)); }


Well, another extension method - which retrieves the actual attribute name:

 public static class DGAExt { public static string GetAttribName(this DGA attrib) { if (_dgaNames != null) return _dgaNames[attrib]; return EnumHelper.GetDescription(attrib); } } public static class EnumHelper { /// <summary> /// Retrieve the description on the enum, eg /// [Description("Bright Pink")] /// BrightPink = 2, /// Then when you pass in the enum, it will retrieve the description /// </summary> /// <param name="en">The Enumeration</param> /// <returns>A string representing the friendly name</returns> public static string GetDescription(Enum en) { var desc = GetAttribute<DescriptionAttribute>(en); if (desc != null) return desc.Description; return en.ToString(); } public static T GetAttribute<T>(Enum en) where T : System.Attribute { Type type = en.GetType(); MemberInfo[] memInfo = type.GetMember(en.ToString()); if (memInfo.Length > 0) { var attrs = memInfo[0].GetCustomAttributes(typeof(T), false).Cast<T>(); return attrs.FirstOrDefault(); } return null; } } 
public static class DGAExt { public static string GetAttribName(this DGA attrib) { if (_dgaNames != null) return _dgaNames[attrib]; return EnumHelper.GetDescription(attrib); } } public static class EnumHelper { /// <summary> /// Retrieve the description on the enum, eg /// [Description("Bright Pink")] /// BrightPink = 2, /// Then when you pass in the enum, it will retrieve the description /// </summary> /// <param name="en">The Enumeration</param> /// <returns>A string representing the friendly name</returns> public static string GetDescription(Enum en) { var desc = GetAttribute<DescriptionAttribute>(en); if (desc != null) return desc.Description; return en.ToString(); } public static T GetAttribute<T>(Enum en) where T : System.Attribute { Type type = en.GetType(); MemberInfo[] memInfo = type.GetMember(en.ToString()); if (memInfo.Length > 0) { var attrs = memInfo[0].GetCustomAttributes(typeof(T), false).Cast<T>(); return attrs.FirstOrDefault(); } return null; } }


Conclusion



With this example I wanted to show that in some cases it is much more convenient to use enumerations than string constants. An enumeration is an explicitly defined type, while string constants for the compiler are all looking the same. For each enumeration, you can declare methods that are specific to this type only. This makes it possible to control the correctness of the use of constants at the compilation stage. Also, using attributes, you can provide each element of the enumeration with additional information that can be used by various methods. For example, in the task that he cited as an example, in the end, the names of some of the search fields had to be placed in the configuration file. The elements of the enumeration corresponding to these fields received an additional attribute containing the key of the entry in the configuration file. Now the function of obtaining the name of the attribute checks for the presence of such an attribute and, if necessary, refers to the configuration file. In this case, the code for generating the query string has not undergone any changes - i.e. All advantages of using constants are preserved.

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


All Articles