Creating a layered design in an application is a fundamental element of modern software architecture. The goal is to promote the Separation of Concerns (SoC) design principle. Separation of Concerns The ideas behind SoC date back to Dijkstra's 1974 paper "On the role of scientific thought" . In computer science, separation of concerns (sometimes abbreviated as SoC) is a design principle for separating a computer program into distinct sections. Each section addresses a separate concern, a set of information that affects the code of a computer program. A concern can be as general as "the details of the hardware for an application", or as specific as "the name of which class to instantiate". A program that embodies SoC well is called a modular program. Modularity, and hence separation of concerns, is achieved by encapsulating information inside a section of code that has a well-defined interface. - Wikipedia SoC is a broad design principal th...
Using Reflection to Retrieve a Property Attribute in .NET: Part 1
Reflection is a powerful tool in .NET that allows for more dynamic programming techniques. In this scenario, we want to retrieve Attributes of class Property.
Take the following class as an example:
public class Reward
{
[Required]
public int Id { get; set; }
[StringLength(32, MinimumLength = 1)]
public string Name { get; set; } = string.Empty;
[StringLength(128, MinimumLength = 1)]
public string Description { get; set; } = string.Empty;
}
This allows us to do things like looking up the StringLength's MaximumLength value programmatically.
This can be done using Reflection. Below is a class that utilizes Reflection to retrieve a property Attribute.
public static class AttributeUtility
{
/// <summary>
/// Return the requested Attribute for the passed propertyName
/// </summary>
/// <typeparam name="TClass">class to be searched</typeparam>
/// <typeparam name="TAttribute">attribute that is being returned</typeparam>
/// <param name="propertyName">PropertyName that we're going to look for an Attribute on</param>
/// <returns>the Attribute if it exists</returns>
public static TAttribute? GetAttribute<TClass, TAttribute>(string propertyName)
{
Type modelType = typeof(TClass);
// first check for standard attribute
var attribute = (TAttribute?)modelType.GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.IgnoreCase)?
.GetCustomAttributes(typeof(TAttribute), true)
.SingleOrDefault();
if (attribute == null)
{
// if the standard attribute is null, look for a metadata attribute
var metadataTypeAttribute = (MetadataTypeAttribute?)modelType.GetCustomAttributes(typeof(MetadataTypeAttribute), true)
.FirstOrDefault();
if (metadataTypeAttribute != null)
{
var property = metadataTypeAttribute
.MetadataClassType
.GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.IgnoreCase);
if (property != null)
{
attribute = (TAttribute?)property.GetCustomAttributes(typeof(TAttribute), true)
.FirstOrDefault();
}
}
}
return attribute;
}
/// <summary>
/// Get the name of a property from the passed expression
/// </summary>
/// <typeparam name="TClass">class to be searched</typeparam>
/// <param name="expression">Expression that points to a class property.</param>
/// <returns>Property name that the expression is referencing</returns>
public static string GetPropertyNameFromExpression<TClass>(Expression<Func<TClass, object>> expression)
{
List<string> propertyList = new List<string>();
//unless it's a root property the expression's NodeType will always be Convert
switch (expression.Body.NodeType)
{
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
var unaryExpression = expression.Body as UnaryExpression;
propertyList = (unaryExpression?.Operand ?? Expression.Empty())
.ToString()
.Split(".")
.Skip(1) // ignore the root property
.ToList();
break;
default:
propertyList = expression.Body
.ToString()
.Split(".")
.Skip(1) // ignore the root property
.ToList();
break;
}
// return the last matching property
return propertyList.LastOrDefault() ?? string.Empty;
}
/// <summary>
/// Return the requested Attribute for the passed expression
/// </summary>
/// <typeparam name="TClass">class to be searched</typeparam>
/// <typeparam name="TAttribute">attribute that is being returned</typeparam>
/// <param name="expression">Expression that points to a class property.</param>
/// <returns>the Attribute if it exists</returns>
public static TAttribute? GetAttribute<TClass, TAttribute>(Expression<Func<TClass, object>> expression)
{
var propertyName = GetPropertyNameFromExpression<TClass>(expression);
return GetAttribute<TClass, TAttribute>(propertyName);
}
}
Let's look at how the first method, GetAttribute, can be used with the sample Reward class we created earlier. We can look up the StringLength attribute and check the MaximumLength property.
// Use the AttributeUtility to get the StringLengthAttribute on the Reward.Name property
var stringLengthAttribute = (StringLengthAttribute?)AttributeUtility.GetAttribute<Reward, StringLengthAttribute>("Name");
if (stringLengthAttribute != null)
{
minimumStringLength = stringLengthAttribute.MinimumLength;
maximumStringLength = stringLengthAttribute.MaximumLength;
}}
The above code is fairly generic, but one problem is that the Property name is hardcoded ("Name"). To fix that, we can update AttributeUtility to accomodate .NET expressions. The GetPropertyNameFromExpression method solves this issue, so we can use the following code to retrieve the StringLength attribute from the Reward class.
// Use the AttributeUtility to get the StringLengthAttribute on the Reward.Name property
var stringLengthAttribute = (StringLengthAttribute?)AttributeUtility.GetAttribute<Reward, StringLengthAttribute>(f => f.Name);
if (stringLengthAttribute != null)
{
minimumStringLength = stringLengthAttribute.MinimumLength;
maximumStringLength = stringLengthAttribute.MaximumLength;
}
This code doesn't rely on a string to lookup the property, which reduces possible runtime errors.
When working with Reflection, keep in mind that it can be slow. However, in scenarios like this, it can reduce runtime errors, so the usage must be balanced.
This example was created using Visual Studio 2022 running a Blazor Web App project.