Google


Thursday, August 02, 2007

Smart O/R Mapper

Smart O/R Mapper (with System.Reflection and Microsoft.Practices.EnterpriseLibrary.Caching)
Download code here

I've combined some ideas from DotNetNuke's CBO (I think this stands for Business Objects Creator or something like this..) and from an MSDN article on custom attributes (Can't remember the link to it), to develop whatI call a smart and agile O/R mapper. I will walk through the several parts of the solution:

First, I will develop a custom attribute with the name ColumnName. This custom attribute , with its AttributeUsage attribute set to AttributeTargets.Property will be used to mark public properties in my business entity class. Let's see how I will mark them: suppose that I have some database table named "Product" with three columns: ProductID, ProductName and ProductPrice, and a business class that will correspond to a row in the Products table as follows:

public class Product
{
private string _id;
private string _name;
private decimal _price;

public string ID

{
get { return _id; }
set { _id = value; }
}
public string Name

{
get { return _name; }
set { _name = value; }
}
public decimal Price

{
get { return _price; }
set { _price = value; }
}
}


In a typical situation, your run through the tedious job, again and again, which is the translation of your data object (holding product records) into a Product object (or Products collection). Using conventional code, you would do this:



using (SqlConnection objConn = new SqlConnection(connectionString))
{

SqlCommand cmd = new SqlCommand("SELECT ProductID, ProductName, ProductPrice FROM Products");
cmd.Connection = objConn;
objConn.Open();
IDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
Product p = null;
if (reader.Read())
{
p = new Product();
p.ID = (string) reader["ProductID"];
p.Name = (string)reader["ProductName"];
p.Price = (decimal)reader["ProductPrice"];

} }
return p;

Let's do it in a smart and agile way! As I said, First, I developed a custom attribute as follows:

namespace MyAttributesNameSpace{ ///


/// Indicates that a class property corresponds to a table field in a database ///

[AttributeUsage(AttributeTargets.Property , AllowMultiple = false)]
class ColumnName : Attribute
{ public readonly string Name;

/// /// Initializes a new instance of the ColumnName class. ///
/// Column name
public ColumnName(string name) { Name = name; } }}

I will use this attribute to mark a class property as corresponding to a column in Products table. The Product class becomes as follows after applying the attribute
public class Product
{
private string _id;
private string _name;
private decimal _price;
[ColumnName("ProductID")]
public string ID
{
get { return _id; }
set { _id = value; }
}

[ColumnName("ProductName")]
public string Name
{
get { return _name; }
set { _name = value; }
}

[ColumnName("ProductPrice")]
public decimal Price
{
get { return _price; }
set { _price = value; }
}
}


Now, the mapper class: This mapper will be used to create an instance of the desired business class at runtime (this enbales us to handle a variety of business classes at once using generic code)

First I will create a method called FillObject as the following:
public static object FillObject(IDataReader dataReader, Type classType)


{
// Create and object based on the type supplied
object businessObject = Activator.CreateInstance(classType);
// Get class properties (we need to populate) from data reader
ArrayList classProperties = GetPropertyInfo(classType);

// Get class property attributes
ColumnName[] classColumns = GetPropertiesAttributes(classProperties);
// Get object column ordinals (accessing a column using an ordinal is faster! So, get the ordinals)
int[] columnOrdinals = GetOrdinals(classType,classColumns, dataReader);


// Loop through properties and set
for (int propertyCount = 0; propertyCount <>

{ // if the ordinal is -1, then no column found on table

if (columnOrdinals[propertyCount] == -1)

{

throw new ApplicationException(Messages.ColumnNotFoundOnReader); }

// Pretty complex, ha?

// Get each property, and set its value from the corresponding field on data reader

// But before you set the value, cast the value to the property type

((PropertyInfo)classProperties[propertyCount]).SetValue(businessObject, CastToAppropriatePropertyType(((PropertyInfo)classProperties[propertyCount]).PropertyType, dataReader[columnOrdinals[propertyCount]]), null); } return businessObject; }

Second, method GetPropertyInfo gets the class properties. Note that we need to get a handle on the properties, in order to populate them.
private static ArrayList GetPropertyInfo(Type classType) {

string classPropertiesCacheKey = string.Concat(classType.FullName, "Properties"); CacheManager myCache = CacheFactory.GetCacheManager();
// Reflection is expensive! get the properties from cache, if available.

ArrayList objProperties = (ArrayList) symphonyCMCache.GetData(classPropertiesCacheKey);

if (objProperties == null)
{ objProperties = new ArrayList();

foreach (PropertyInfo propertyInfo in classType.GetProperties())
{
objProperties.Add(propertyInfo);
}

myCache.Add(classPropertiesCacheKey,objProperties);
}

return objProperties;
}

Third, get the column name (represented by the ColumnName attribute) that corresponds to each property
private static ColumnName[] GetPropertiesAttributes(ArrayList properties)
{
ColumnName[] tableColumns = new ColumnName[properties.Count];
for (int i = 0; i <>
{ PropertyInfo property = (PropertyInfo)properties[i];
// ColumnName is the only attribute on property (so get it using index 0)
tableColumns[i] = (ColumnName)property.GetCustomAttributes(typeof(ColumnName), false)[0];
}
//return tableColumns;
return tableColumns;
}
After we obtained the columns corresponding to the properties, we will get these columns ordinals

private static int[] GetOrdinals(Type classType,ColumnName [] columns, IDataReader dataReader)
{
string classOrdinalsCacheKey = string.Concat(classType.FullName, "Ordinals");
CacheManager myCache = CacheFactory.GetCacheManager();
// Why get ordinals each time? cache ordinals for better performance
int[] arrOrdinals = (int[])myCache.GetData(classOrdinalsCacheKey);
if (arrOrdinals == null) {
if (dataReader != null) {
arrOrdinals = new int[columns.Length];
for (int column = 0; column <>
{ arrOrdinals[column] = -1;
try
{
arrOrdinals[column] = dataReader.GetOrdinal(columns[column].ColumnName);
}
catch (Exception ex)
{ // property does not exist in datareader }
} }
// Cache class ordinals
myCache.Add(classOrdinalsCacheKey, arrOrdinals);
}
return arrOrdinals;
}
Fifth, we will loop through the properties, and set them (see function FillObject code).
And last, call the mapper to create an object from a data reader:
using (SqlConnection conn = new SqlConnection(_connectionString))
{ SqlCommand cmd = new SqlCommand("SELECT * FROM Products");
cmd.Connection = conn;
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
Product p = (Product)DataAccessHelper.FillObject(reader, typeof(Product));
productsCollection.Add(product);
}
Hope you will enjoy it as I did! Cheers
*[If you think that you have valuable feedback to improve the contents of this blog, please leave it as a comment here]


1 Comments:

At 5:47 AM, Anonymous Anonymous said...

This looks a bit like the code-behind that Dunn & Churchill Diamond Binding generates. We've been using it for a couple of projects now, and you are right - ORM is the way of the future

Authors site here. Diamond Binding does a load of other stuff like relationships and inheritance though - its pretty cool. Its like $400 though... (can't use the free version for business projects) :( My boss pays the bills anyway I guess ;)

Cheers,
AD

 

Post a Comment

<< Home