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 gives 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 similiar to the DotNetNuke CBO class.
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 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 precached 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 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 an input of IDataReader, and a return type of object, hence a Func<IDataReader, object>.
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 referes to variables of the outer function).
Creating the Property Mapping Functions
Remember that our goal here is to programmatically work out how 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
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);
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 also is 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 may look like might work.
Points of Interest
Lamda Expressions are a extremely powerful new feature of C# 3.0. Use it!
History
|
|
 |
 | why Action delegate PrerakKaushik | 10:45 11 Feb '10 |
|
 |
very good article, i have small question? Why do we need Action Delegate inside CreateObjectMapping [1]. Was it possible to do without using CreatePropertyMapping[2] (perhaps more code in 1). Was that done for code readability ? Thanks
|
|
|
|
 |
|
 |
Yes it was done for readability.
|
|
|
|
 |
 | Performance Izzet Kerem Kusmezer | 2:14 7 Dec '09 |
|
 |
You totally depend on reflection during setting up the property values, the speed will not be that fast.
|
|
|
|
 |
|
 |
Yes this is true. For small to medium sized database applications I find that the convenience of this layer outweighs the performance cost.
Perhaps another way would be to something like CodeDom to programmatically create the mapping classes dynamically followed by a compilation.
|
|
|
|
 |
 | update/insert? damianrda | 9:40 5 Aug '08 |
|
 |
how would you go about inserting or updating data back into the db
|
|
|
|
 |
|
 |
That part I haven't published. THis would involve translating a type into a insert, update statement and running it.
If you want to be very lazy, you'll also check for table existance, create the table if it doesn't exist, and/or create stored procs.
|
|
|
|
 |
 | Nice use of new C# 3 features
leppie
| 22:41 17 Dec '07 |
|
 |
It's been done a 1000 times, but I like the use of the new C# 3.0 features. Type inference and lambdas, yummy
xacc.ideIronScheme a R5RS/R6RS-compliant Scheme on the DLR
The rule of three: "The first time you notice something that might repeat, don't generalize it. The second time the situation occurs, develop in a similar fashion -- possibly even copy/paste -- but don't generalize yet. On the third time, look to generalize the approach."
|
|
|
|
 |
|
 |
haha thanks, from what I've seen at the places I've worked at, ORM using Reflection is often rejected as too slow. This implementation should be just as fast as hard coded ORM except for the first hit. For me the possibility of having massive code reduction on the DAL is very hard to ignore.
|
|
|
|
 |
|
 |
Thanks for publishing a very useful article. The projects we are doing at the moment is still using .NET 2.0. Would you be kind enough to post a version that will work with version 2.0? Many thanks in advance!
|
|
|
|
 |
|
 |
Great work Andrew! I don't know if it is wrong or not, but it seems to work just fine. This is provided as is and on a "use at your own risk" basis, etc, etc... Replace the Helper class with this one.
using System; using System.Collections.Generic; using System.Text; using System.Reflection; using System.Data;
namespace ReflectionOrmLayer { public delegate object ReturnObject(IDataReader dr); public delegate void SetValue(IDataReader dr, object obj);
public static class Helper { private static Dictionary<string, ConstructorInfo> _constructorCache = new Dictionary<string, ConstructorInfo>(); private static Dictionary<string, PropertyInfo[]> _propertyInfoCache = new Dictionary<string, PropertyInfo[]>(); private static Dictionary<string, ReturnObject> _mappingCache = new Dictionary<string, ReturnObject>();
public static ConstructorInfo GetDefaultConstructor<T>() { Type type = typeof(T);
if (!_constructorCache.ContainsKey(type.FullName)) { _constructorCache.Add(type.FullName, type.GetConstructor(Type.EmptyTypes)); } return _constructorCache[type.FullName]; }
private static T CreateInstance<T>() { ConstructorInfo constructor = GetDefaultConstructor<T>(); return (T)constructor.Invoke(new object[0]); }
static string GetMappingSignature(IDataReader dr, Type type) { DataTable table = dr.GetSchemaTable(); StringBuilder builder = new StringBuilder();
foreach (DataColumn c in table.Columns) { builder.Append(c.ColumnName); }
builder.Append(type.FullName);
return builder.ToString(); }
private static SetValue CreateSetValueAction(PropertyInfo info, Type columnType, string columnName) { Type propType = info.PropertyType; SetValue setValue; if (propType.BaseType.Equals(typeof(Enum))) { setValue = delegate(IDataReader dr, object obj) { if (IsNumeric(dr[columnName])) info.SetValue(obj, Enum.ToObject(propType, Convert.ToInt32(dr[columnName])), null); else
info.SetValue(obj, Enum.ToObject(propType, dr[columnName]), null); }; } else if (propType.Equals(columnType)) { setValue = delegate(IDataReader dr, object obj) { info.SetValue(obj, dr[columnName], null); }; } else
{ setValue = delegate(IDataReader dr, object obj) { info.SetValue(obj, Convert.ChangeType(dr[columnName], propType), null); }; }
return delegate(IDataReader dr, object obj) { if (Convert.IsDBNull(dr[columnName])) info.SetValue(obj, Null.GetNull(info), null); else
setValue(dr, obj); }; }
public static T CreateObject<T>(IDataReader dr) { string mappingSignature = GetMappingSignature(dr, typeof(T));
if (!_mappingCache.ContainsKey(mappingSignature)) { _mappingCache.Add(mappingSignature, CreateObjectMapping<T>()); }
return (T)_mappingCache[mappingSignature](dr); }
private static ReturnObject CreateObjectMapping<T>() { return delegate(IDataReader dr) { object obj = CreateInstance<T>(); List<SetValue> mapping = CreatePropertyMapping<T>(dr); foreach (SetValue setValue in mapping) { setValue(dr, obj); } return obj; }; }
private static List<SetValue> CreatePropertyMapping<T>(IDataRecord dr) { List<PropertyInfo> properties = GetPropertyInfo<T>(); List<SetValue> mapping = new List<SetValue>();
for (int i = 0; i < dr.FieldCount; i++) { string columnName = dr.GetName(i); PropertyInfo propMatch = properties.Find(delegate(PropertyInfo p) { return p.Name.ToLower() == columnName.ToLower(); }); if (propMatch != null) { Type columnType = dr.GetFieldType(i); mapping.Add(CreateSetValueAction(propMatch, columnType, columnName)); } } return mapping; }
public static bool IsNumeric(object expression) { double retNum;
bool isNum = Double.TryParse(Convert.ToString(expression), System.Globalization.NumberStyles.Any, System.Globalization.NumberFormatInfo.InvariantInfo, out retNum); return isNum; }
public static List<PropertyInfo> GetPropertyInfo<T>() { Type type = typeof(T);
if (!_propertyInfoCache.ContainsKey(type.FullName)) { _propertyInfoCache.Add(type.FullName, type.GetProperties()); }
return new List<PropertyInfo>(_propertyInfoCache[type.FullName]); }
public static int ExecuteNonQuery<T>(IDbCommand command, T item) { FillParameters<T>(item, command); return command.ExecuteNonQuery(); }
public static void FillParameters<T>(object obj, IDbCommand command) { List<PropertyInfo> properties = GetPropertyInfo<T>();
foreach (IDbDataParameter dbDataParameter in command.Parameters) { IDbDataParameter param = dbDataParameter; PropertyInfo property = properties.Find(delegate(PropertyInfo p) { return p.Name.ToLower() == param.SourceColumn.ToLower(); });
object value = property.GetValue(obj, null);
dbDataParameter.Value = Null.IsNull(value) ? DBNull.Value : value; } } } }
|
|
|
|
 |
|
 |
Many thanks! Much appreciated!!
|
|
|
|
 |
|
|
Last Updated 17 Dec 2007 |
Advertise |
Privacy |
Terms of Use |
Copyright ©
CodeProject, 1999-2010