Introduction
Sometimes, we find it necessary to build classes that contain dozens if not over a hundred separate data fields. It may be ugly, but sometimes it's still the best approach. By combining custom attributes with Reflection, we can eliminate the grunt work and a lot of boilerplate from writing and instantiating very large classes.
All you need for this approach to work is to get your data in key/values pairs (e.g. a URL query string).
Background
I developed this code while writing a class to store the data returned from a credit card transaction processing gateway. What they give you back can be considered a mini-data-dump. While perusing the vendor's documentation, I found an example of what they post back to my web server. After copying the list of fields into Excel, I discovered that it contains 100 distinct fields. Yes, 100! Since I don't have a clue how the data will be used downstream yet, I figured it would be best to make it possible to capture all of the data they sent. Since the class was going to contain 100 or more individual properties, I imagined that there has to be some way to make populating all the data a lot simpler.
Using the Code
The first thing I did was to define a custom attribute that I can use to decorate each property. The attribute contains a single string
which just happens to be the key value of my data that is transmitted over-the-wire.
internal class TransactionRelayFieldAttribute : Attribute
{
private string _fieldName;
public TransactionRelayFieldAttribute(string fieldName)
{
_fieldName = fieldName;
}
public string Name { get { return _fieldName; } }
}
What this attribute allows me to do is to "decorate" all of the fields in my large class like this:
[TransactionRelayField("x_response_code")]
public string ResponseCode { get; set; }
In this example, I am able to provide a lot of the information necessary to figure out what this code is doing right in the code. This can be helpful for the next person who looks at your enormous class because they can see what it's all about right up front.
With my current project, I wrote out 100 hundred of these fields. The properties of the class alone took up nearly 800 lines of code.
Leveraging Reflection
By using reflection, I do not have to write out a lengthy function to process each possibility. Could you imagine a switch
...case
with 100 different paths. Whoever maintains this code after me would hunt me down and do bad things.
static Dictionary<string, PropertyInfo> _propertyMap;
private static MapProperties()
{
_propertyMap = new Dictionary<string, PropertyInfo>();
Type relayPost = typeof(TransactionRelay)
foreach(PropertyInfo pInfo in relayPost.GetProperties())
{
foreach (object attr in pInfo.GetCustomAttributes())
{
var field = attr as TransactionRelayFieldAttribute;
if (field != null)
{
_propertyMap.Add(field.Name, pInfo);
break;
}
}
}
}
static RelayPost()
{
MapProperties();
}
Here, I used statics and Reflection to create and populate a property map of all 100 fields before the first object was ever created.
Next, when I need to instantiate an actual instance of this gigantic class, all I need is this function:
private NameValueCollection _unhandledValues;
private RelayPost() {}
public static RelayPost Create(NameValueCollection postValues)
{
RelayPost post = new RelayPost();
string key;
string value;
post._unhandledValues = new NameValueCollection();
for (int i = 0; i < postValues.Count; ++i)
{
key = postValues.Keys[i];
value = postValues[i];
try
{
_propertyMap[key].SetValue(post, value);
}
catch (Exception)
{
post._unhandleValues.Add(key, value);
}
}
return post;
}
And that's it. Populating a hundred fields was never so easy.
Putting it all together
So here is what it looks likes when you put it all together. I will write this in the context of my current project for clarity.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Reflection;
namespace Website.Models
{
internal class TransactionRelayFieldAttribute : Attribute
{
private string _fieldName;
public TransactionRelayFieldAttribute(string fieldName)
{
if (String.IsNullOrWhiteSpace(fieldName))
throw new ArgumentNullException("fieldName");
_fieldName = fieldName;
}
public string Name { get { return _fieldName; } }
}
public class RelayPost
{
private static Dictionary<string, PropertyInfo> _propertyMap;
private NameValueCollection _unhandledKeys;
private RelayPost() { }
public NameValueCollection UnhandledKeys { get { return _unhandledKeys; } }
[TransactionRelayField("x_response_code")]
public string ResponseCode { get; set; }
static RelayPost()
{
CreatePropertyMap();
}
private static void CreatePropertyMap()
{
_propertyMap = new Dictionary<string, PropertyInfo>();
Type relayPost = typeof(RelayPost);
foreach (PropertyInfo pInfo in relayPost.GetProperties())
{
foreach (object attr in pInfo.GetCustomAttributes())
{
var field = attr as TransactionRelayFieldAttribute;
if (field != null)
{
_propertyMap.Add(field.Name, pInfo);
break;
}
}
}
}
public static RelayPost Create(NameValueCollection postValues)
{
if (postValues == null)
throw new ArgumentNullException("postValues");
var post = new RelayPost();
string key;
string value;
post._unhandledKeys = new NameValueCollection();
for (int i = 0; i < postValues.Count; ++i)
{
key = postValues.Keys[i];
value = postValues[i];
try
{
_propertyMap[key].SetValue(post, value);
}
catch (Exception)
{
post._unhandledKeys.Add(key, value);
}
}
return post;
}
}
}
Points of Interest
The thing I like about Reflection is that it's kind of like C++. You can accomplish all sorts of fun things with it but, on the other hand, it gives you enough rope to hang yourself. It gives you ways to short-cut all sorts of code but beware, your mileage may vary.
History
- 12/22/2017: Initial posting
- 12/26/2017: Minor syntax change. Added a more complete code example.
- 12/28/2017: A few fixes in sample code.