November 23, 2024

Creating Lookup Data From a C# Class

Hard-coding constant values in C# classes is a bad practice that we rarely indulge in here at TAMUS ESI.  You will not see very many instances of code like:

    if (someObject.StatusCode == "A")

We prefer something more like:

    if (someObject.StatusCode == SomeProjectConstants.SomeFieldStatus.Active)

where SomeFieldStatus is a class like this one in HRConnect 2:

    public static class BppLogFieldCodes         
{            
public const string FIT = "FIT";
public const string ACH = "ACH";
...
}

Nothing fancy about this, but creating classes/constants like these takes only moments and helps reduce bugs and makes changing/adding new codes easier in future releases.

What isn’t so helpful about this class – or any other basic C# class – is that it doesn’t provide any metadata about the code values. For example, it would be very convenient if the "friendly description" for the FIT code were directly associated with the code property in the BppLogFieldCodes class.

The usual technique for getting code/description translation data into an ESI web application is to use a "Lookups" class that knows how to connect to a data source, read a database table (or tables), and create a list of key/value pairs.  The Lookups class will also normally provide caching of the resulting queries to improve performance.  The Lookup data is then used to populate dropdowns, radio button lists, etc.  Again, nothing fancy.

Such Lookup classes are all well and good, but having cached lookup data does nothing to eliminate the BppLogFieldsCode class and others like it – the constants are still needed for comparisons, etc., as initially demonstrated.

But what if we could create a class like this one that includes the lookup metadata in the class definition?

    public static class BppLogFieldCodes         
{            
[Description("Federal Income Tax")]            
public const string FIT = "FIT";

[Description("Direct Deposit")]            
public const string ACH = "ACH";
...
}

Thanks to the new AttributeInterrogator class in the So.Esi.Core assembly, it is now possible to annotate your class as shown, then write a simple, one-line method to get a list of lookup data for use in your app’s UI.  For example:

    public IList<LookupDataDTO> GetBppLogFieldSets()         
{            
return MapAttributeListToLookupData(                
AttributeInterrogator.GetTypeAttributes<DescriptionAttribute>(
typeof(HRConnectWebConstants.BppLogFieldCodes)));        
}

The GetBppLogFieldSets method can be used in exactly the same way as methods in the "Lookups" data wrapper class are used now, but no database connection/access is required.

So how is this accomplished? 

The So.Esi.Core assembly contains a DescriptionAtttribute class that inherits from System.Attribute and accepts a description as a parameter to its constructor.  That allows the [Description] attribute to be applied to a class and its properties. 

The Description attribute can also be applied to a C# enum and its fields.  To make enums even more descriptive, a CodeDescription attribute was created to handle the "three-way" mapping that often takes place with an enumeration. 

In the three-way enum scenario, the three values in play are:

  1. The enum value itself
  2. The lookup code
  3. The friendly description

For example, consider the following enum modeled on TrainTraq 3:

    [Description("Role")]         
public enum CodedRatingEnum
{
[CodeDescription("APP-EMPLOYEE", "Employee")]
Employee = 1,

[CodeDescription("APP-DEPT-ADM", "Department Admin")]
Good = 2,
...

         }

The AttributeInterrogator class helps make this metadata convenient to consume.  It also allows developers to create their own custom metadata attribute classes and apply them in their applications.

A quick look at the current implementation of the GetPropertyAttributes method, which is used to scan a class for its metadata, demonstrates how it works beneath the covers:

    public static IList<AttributeInformation<T>> GetPropertyAttributes<T>(object source) 
where T : System.Attribute
{
IList<AttributeInformation<T>> result = null;

Type sourceType = source.GetType();
MemberInfo[] members = sourceType.GetMembers();
foreach (MemberInfo member in members)            
{       
          if (member.MemberType == MemberTypes.Property)
{
                object[] attrs = member.GetCustomAttributes(typeof(T), false);
                if (attrs != null && attrs.Length > 0)
                {
if (result == null)
                        result = new List<AttributeInformation<T>>();

object value = sourceType.GetProperty(member.Name).GetValue(source, null);
                    result.Add(new AttributeInformation<T>
{
Name = member.Name,
Value = value,
Attributes = attrs[0] as T
});
                }
}
        }            
return result;
}

AttributeInformation is a simple class in So.Esi.Core:

    public class AttributeInformation<T> 
where T : class
{
public string Name { get; set; }
        public object Value { get; set; }
        public T Attributes { get; set; }
}

It is notable only for the Attributes property, which is generic.  This is what allows application developers to define their own custom attribute classes for use with the AttributeInterrogator.

The AttributeInterrogator.GetPropertyAttributes method uses reflection to iterate over the specified class’ properties.  It then interrogates over each property to see if a custom attribute of the desired type T is defined.  If so, the property name, value, and attribute information is captured in the result list and returned. 

Similar approaches are taken for interrogating class types and enums.

Leave a Reply