Click here to Skip to main content
15,884,629 members
Articles / Programming Languages / C#

High Performance Reflection ORM Layer

Rate me:
Please Sign up or sign in to vote.
4.59/5 (9 votes)
17 Dec 2007CPOL3 min read 62.3K   647   45   13
Building a High Performance ORM Layer using ADO.NET, Reflection and Lambda Expressions

Introduction

Mapping between a relational database and objects is traditionally a tedious exercise of mapping rows in a database to properties in a class. Many solutions have been created to make ORM a more pleasant experience for developers as a result. Automated code generation tools such as CodeSmith make ORM job easier by generating (data access layer) DAL code but results in a bloated DAL with thousands of lines of code. Frameworks such as NHibernate push mapping to XML and also give the data access layer database independence, but the cost is a relatively heavy code for data access and still the need to map columns into properties. So what other solutions are there? Reflection!

Reflection is a much neglected way of providing an ORM layer. While the cost of reflection is heavy, if the mapping between the target object and the table is cached, theoretically the cost of reflection is only incurred on the first call, with all subsequent calls being retrieved from a cache. In this article, I will go through how to write an efficient ORM using the System.Reflection namespace and C# 3.0's new lambda expressions.

Using the Code

Main Component Entry Point

The objective of our ORM layer is to give a nice way to translate from relational to a given strongly typed object. The interface I used is similar to the DotNetNuke CBO class.

C#
private static Dictionary<string, Func<IDataReader, object>> _mappingCache = 
	new Dictionary<string, Func<IDataReader, object>>();
public static T CreateObject<T>(IDataReader dr)
{
    string mappingSignature = dr.GetMappingSignature(typeof(T)); 
    if (!_mappingCache.ContainsKey(mappingSignature))
    {
        _mappingCache.Add(mappingSignature, CreateObjectMapping<T>(dr));
    } 
    return (T)_mappingCache[mappingSignature].Invoke(dr);
} 

This is the main entry method of our component. We give the static method a type T, and also pass in an IDataReader which contains our relational data. The GetMappingSignature basically creates a string signature to uniquely identify the mapping between the given columns and the destination type. This is used so we can retrieve pre-cached mapping delegates later, bypassing the expensive reflection step. Our mapping cache is a lookup to find pre cached functions which provide the mapping between the IDataReader and the object.

Creating the Object Mapping Function

The essence of what we are trying to create is a function which can return an object from a given row of data. Since we want to cache this construct for future use, we need a function which is an input of IDataReader, and a return type of object, hence a Func<IDataReader, object>.

C#
static Func<IDataReader, object> CreateObjectMapping<T>(IDataReader dataReader)
{
    List<Action<IDataReader, object>> mapping = CreatePropertyMapping<T>(dataReader); 
    return (IDataReader dr) =>
    {
        object obj = CreateInstance<T>(); 
        
        for (int i = 0; i < mapping.Count; i++)
        {
            Action<IDataReader, object> action = mapping[i]; 
            action.Invoke(dr, obj);
        } 
        return obj;
    };
} 

This function does the bulk of the work in forming our mapping between the IDataReader and the object. It returns a function which creates an object of the given type with all its fields filled by the IDataReader. The List<Action<IDataReader, object>> contains the collection of assignment operations for each property in the type. CreateInstance<T>() is a method which creates an instance of a given type using a cached default constructor. A lambda expression is used to create a function dynamically using the mapping list in a closure (when an inner function refers to variables of the outer function).

Creating the Property Mapping Functions

Remember that our goal here is to programmatically work out how to create data objects using only type information form our type T and relational information in the IDataReader. This mapping basically involves setting an object property with the values inside the corresponding column. To do this, we will map our object property names against the column names

C#
static List<Action<IDataReader, object>> CreatePropertyMapping<T>(IDataReader dr)
{
    List<PropertyInfo> properties = Helper.GetPropertyInfo<T>();
    List<Action<IDataReader, object>> mapping = 
		new List<Action<IDataReader, object>>(); int i;
    for (i = 0; i < dr.FieldCount; i++)
    {
        string columnName = dr.GetName(i);
        //now find matching property
        PropertyInfo propMatch = (from p in properties
                        where p.Name.ToLower() == columnName.ToLower()
                        select p).FirstOrDefault();
        if (propMatch != null)
        {
            Type columnType = dr.GetFieldType(i);
            Action<IDataReader, object> action = 
		CreateSetValueAction(propMatch, columnType, columnName);
            mapping.Add(action);
        }
    }
    return mapping;
} 

Helper.GetPropertyInfo<T>() basically retrieves the property collection from a given type. CreateSetValueAction creates our assignment action based on the source data type and destination datatype. It is also responsible for working out type conversions for Enumerations and other things.

Usage of the Component

While the CreateObject<T>(IDataReader dr) function does the bulk of the work, it is necessary to use this component from a higher layer. I have included a small sample of how such a layer might work.

Points of Interest

Lambda Expressions are an extremely powerful new feature of C# 3.0. Use them!

History

  • 17th December, 2007: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
emoulin13-Nov-13 21:52
emoulin13-Nov-13 21:52 
GeneralMy vote of 5 Pin
Eugene Sadovoi2-Apr-12 8:42
Eugene Sadovoi2-Apr-12 8:42 
Questionwhy Action<T,..> delegate Pin
PrerakKaushik11-Feb-10 9:45
PrerakKaushik11-Feb-10 9:45 
AnswerRe: why Action delegate Pin
Andrew Chan11-Feb-10 12:11
Andrew Chan11-Feb-10 12:11 
GeneralPerformance Pin
Izzet Kerem Kusmezer7-Dec-09 1:14
Izzet Kerem Kusmezer7-Dec-09 1:14 
GeneralRe: Performance Pin
Andrew Chan11-Feb-10 12:15
Andrew Chan11-Feb-10 12:15 
Questionupdate/insert? Pin
damianrda5-Aug-08 8:40
damianrda5-Aug-08 8:40 
AnswerRe: update/insert? Pin
Andrew Chan19-Nov-08 14:51
Andrew Chan19-Nov-08 14:51 
GeneralNice use of new C# 3 features Pin
leppie17-Dec-07 21:41
leppie17-Dec-07 21:41 
GeneralRe: Nice use of new C# 3 features Pin
Andrew Chan18-Dec-07 0:19
Andrew Chan18-Dec-07 0:19 
GeneralORM Layer code for .NET 2.0 Pin
arakkots7-Feb-09 1:14
arakkots7-Feb-09 1:14 
GeneralRe: ORM Layer code for .NET 2.0 Pin
wilfre12-Aug-09 11:04
wilfre12-Aug-09 11:04 
GeneralRe: ORM Layer code for .NET 2.0 Pin
arakkots27-Aug-09 12:43
arakkots27-Aug-09 12:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.