The current project I’m working on interfaces with a third-party screen-scraping robot. Actually, lots of them, it could be close to a thousand by the time all is said and done. The challenge was to create some sort of usable interface between the value structures the robot returns and the actual business objects that make up my app. In addition, the naming of those value structures don’t match my business objects.
For example, if I was using a robot for a banking site, it might take in four parameters, say “user_name”, “password”, “account_number” and “account_type” and return three parameters, “last_login_date”, “current_balance” and “site_messages.” Of course, last_login_date and current_balance would be strings, not date and double objects because of the way the robots are. Also, because of the size of the app, and because I only want the robot to get the information and not have to worry about anything else with it, I wanted a way to convert the values to a usable object, say, a DataSet.
In my first stab at it, I created an object mapper that used reflection to create a DataTable for each unique object type returned by the object, with each table containing a row for each object of that type returned. I then created an object mapper for each type of robot that would take the DataSet and convert it to the object type I needed.
So, extending the example from above, I might have two bank robots, one for Example Bank and one for Joe’s Bank. One robot might look for an ExampleBankInput object and return ExampleBankOutput objects, while the other would look for JoesBankInput object and return JoesBankOutput objects. However, these input and output objects didn’t necessarily relate to the business objects I wanted to use in my app. I want user_name to be UserName and password to be Password and both of them to be in a User object so that I don’t end up with 7,000 different objects that all do basically the same thing. But then I want last_login_date to be LastLoginDate and be a DateTime field that is in an object specific to the bank I am working with because each bank might implement it differently.
So to make this work, I created more object mappers, one for each special type that I had. It only took me creating about three of these to realize there had to be a better way to do it. How can I create a generic mapping class that knows how to convert all of my business objects back and forth, without me having to write complex custom logic?
The answer? Method Attributes.
If you haven’t seen them before, c# allows you to attach attributes to methods:
[MyCustomAttribute("blah")]
public void MyMethod()
{
//some stuff here
}
In the above example, MyCustomAttribute would be a class that had a constructor that took a single parameter of a string. How does that help? Well, because in c# Properties are also methods, you can apply custom attributes to properties. Hence a customizable value object.
public class BankUser
{
private string userName;
private string password;
public BankUser(string userName, string password)
{
this.userName = userName;
this.password = password;
}
[BankRobotInputAttribute("user_name")]
[BankRobotOutputAttribute("user_name")]
public string UserName
{
get{return userName;}
set{userName = value;}
}
[BankRobotInputAttribute("password")]
[BankRobotOutputAttribute("password")]
public string Password
{
get{return password;}
set{password = value;}
}
}
So then, all I have to do is write a mapper that takes in an object, reads through it’s properties to get custom attributes for the direction the value is heading, and get the value from the property. This is a piece of cake for string values, but what about other types, say DateTime and double like I mentioned earlier? The workaround I used was to create a property just for handling string representations of these values:
public class BankAccount
{
private DateTime lastLoginDate;
private double accountBalance;
public BankAccount() {}
public DateTime LastLoginDate
{
get{return lastLoginDate;}
set{lastLoginDate = value;}
}
public double AccountBalance
{
get{return accountBalance;}
set{accountBalance = value;}
}
[BankRobotOutputAttribute("last_login_date")]
public string s_LastLoginDate
{
set
{
if(value != null && value != String.Empty)
{
try
{
lastLoginDate=DateTime.Parse(value);
}
catch {}
}
}
}
[BankRobotOutputAttribute("account_balance")]
public string s_AccountBalance
{
set
{
if(value != null && value != String.Empty)
{
try
{
accountBalance = double.Parse(value);
}
catch {}
}
}
}
}
This lets me create a mapper like
public static Object FillObject(Object obj, DataSet ds, String tableName)
{
DataTable table = ds.Tables[tableName];
DataRow row = table.Rows[0];
foreach(PropertyInfo prop in obj.GetType().GetProperties())
{
object[] customAttributes =
prop.GetCustomAttributes(typeof(BankRobotOutputAttribute),true);
if(customAttributes.Length > 0)
{
BankRobotOutputAttribute a =(BankRobotOutputAttribute)customAttributes[0];
if(table.Columns.Contains(a.FieldName))
{
DataColumn col = table.Columns[a.FieldName];
prop.SetValue(obj,row[col], null);
}
}
}
return obj;
}