Here's another version
* does 2-pass match as described
* handles null reference-type values (string, byte[])
* handles null Nullable<T> value-type values (int? etc)
* handles (for entity T) class, struct and Nullable<struct>
* caches and re-uses compiled delegates (thread-safe)
I'm not going to advise on the metadata aspect since I don't know (and don't
wish to know) enough about the exact model used by this project.
Probably my last long code-dump on this topic (but feel free to ask follow
ups etc) - I don't want to turn this forum into a repo change-log ;-p
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
class Employee
{
public string Forename { get { return FirstName; } set { FirstName =
value; } }
private string FirstName;
public string LastName { get; set; }
private int EmloyeeID { get; set; }
public DateTime BirthDate { get; set; }
public int? Foo { get; set; }
public string bar { get; set; }
}
/// <summary>
/// Compares arrays of objects using the supplied comparer (or default is
none supplied)
/// </summary>
class ArrayComparer<T> : IEqualityComparer<T[]> {
private readonly IEqualityComparer<T> comparer;
public ArrayComparer() : this(null) { }
public ArrayComparer(IEqualityComparer<T> comparer)
{
this.comparer = comparer ?? EqualityComparer<T>.Default;
}
public int GetHashCode(T[] values)
{
if (values == null) return 0;
int hashCode = 1;
for (int i = 0; i < values.Length; i++)
{
hashCode = (hashCode * 13) + comparer.GetHashCode(values);
}
return hashCode;
}
public bool Equals(T[] lhs, T[] rhs)
{
if (ReferenceEquals(lhs, rhs)) return true;
if (lhs == null || rhs == null || lhs.Length != rhs.Length) return
false;
for (int i = 0; i < lhs.Length; i++)
{
if (!comparer.Equals(lhs, rhs)) return false;
}
return true;
}
}
/// <summary>
/// Responsible for creating and caching reader-delegates for compatible
/// column sets; thread safe.
/// </summary>
static class InitializerCache<T>
{
static readonly Dictionary<string[], Func<IDataReader, T>> readers
= new Dictionary<string[], Func<IDataReader, T>>(
new ArrayComparer<string>(StringComparer.InvariantCulture));
public static Func<IDataReader, T> GetInitializer(string[] names)
{
if (names == null) throw new ArgumentNullException();
Func<IDataReader, T> initializer;
lock (readers)
{
if (!readers.TryGetValue(names, out initializer))
{
initializer = CreateInitializer(names);
readers.Add((string[])names.Clone(), initializer);
}
}
return initializer;
}
private static Func<IDataReader, T> CreateInitializer(string[] names)
{
Trace.WriteLine("Creating initializer for: " + typeof(T).Name);
if (names == null) throw new ArgumentNullException("names");
var readerParam = Expression.Parameter(typeof(IDataReader),
"reader");
Type entityType = typeof(T),
underlyingEntityType = Nullable.GetUnderlyingType(entityType) ??
entityType,
readerType = typeof(IDataRecord);
List<MemberBinding> bindings = new List<MemberBinding>();
Type[] byOrdinal = { typeof(int) };
MethodInfo defaultMethod = readerType.GetMethod("GetValue",
byOrdinal),
isNullMethod = readerType.GetMethod("IsDBNull", byOrdinal);
NewExpression ctor = Expression.New(underlyingEntityType); // try
this first...
for (int ordinal = 0; ordinal < names.Length; ordinal++)
{
string name = names[ordinal];
// TODO: apply mapping here (via attribute?)
// get the lhs of a binding
const BindingFlags FLAGS = BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic;
MemberInfo member =
(MemberInfo)underlyingEntityType.GetProperty(name, FLAGS) ??
(MemberInfo)underlyingEntityType.GetField(name, FLAGS) ??
(MemberInfo)underlyingEntityType.GetProperty(name, FLAGS |
BindingFlags.IgnoreCase) ??
(MemberInfo)underlyingEntityType.GetField(name, FLAGS |
BindingFlags.IgnoreCase);
if (member == null) continue; // doesn't exist
Type valueType;
switch (member.MemberType)
{
case MemberTypes.Field:
valueType = ((FieldInfo)member).FieldType;
break;
case MemberTypes.Property:
if (!((PropertyInfo)member).CanWrite) continue; // read
only
valueType = ((PropertyInfo)member).PropertyType;
break;
default:
throw new
NotSupportedException(string.Format("Unexpected member-type: {0}",
member.MemberType));
}
Type underlyingType = Nullable.GetUnderlyingType(valueType) ??
valueType;
// get the rhs of a binding
MethodInfo method = readerType.GetMethod("Get" +
underlyingType.Name, byOrdinal);
Expression rhs;
if (method != null && method.ReturnType == underlyingType)
{
rhs = Expression.Call(readerParam, method,
Expression.Constant(ordinal, typeof(int)));
}
else
{
rhs = Expression.Convert(Expression.Call(readerParam,
defaultMethod, Expression.Constant(ordinal, typeof(int))), underlyingType);
}
if (underlyingType != valueType)
{ // Nullable<T>; convert underlying T to T?
rhs = Expression.Convert(rhs, valueType);
}
if (underlyingType.IsClass || underlyingType != valueType)
{ // reference-type of Nullable<T>; check for null
// (conditional ternary operator)
rhs = Expression.Condition(
Expression.Call(readerParam, isNullMethod,
Expression.Constant(ordinal, typeof(int))),
Expression.Constant(null, valueType), rhs);
}
bindings.Add(Expression.Bind(member, rhs));
}
Expression body = Expression.MemberInit(ctor, bindings);
if (entityType != underlyingEntityType)
{ // entity itself was T? - so convert
body = Expression.Convert(body, entityType);
}
return Expression.Lambda<Func<IDataReader, T>>(body,
readerParam).Compile();
}
}
static class Program
{
[STAThread]
static void Main()
{
Go();
Go();
Go();
}
static void Go()
{
foreach (Employee emp in Read<Employee>("SELECT FirstName, LastName,
NULL AS Foo, NULL AS bar FROM Employees"))
{
Console.WriteLine("{0} {1}", emp.Forename, emp.LastName);
}
}
const string CS = "TODO"; // northwind
static IEnumerable<T> Read<T>(string command)
{
using (SqlConnection conn = new SqlConnection(CS))
using (SqlCommand cmd = conn.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.CommandText = command;
conn.Open();
using (SqlDataReader reader =
cmd.ExecuteReader(CommandBehavior.CloseConnection |
CommandBehavior.SingleResult))
{
if (reader.Read())
{
string[] names = new string[reader.FieldCount];
for(int i = 0 ; i < names.Length ; i++) {
names = reader.GetName(i);
}
Func<IDataReader, T> objInit =
InitializerCache<T>.GetInitializer(names);
do
{ // walk the data
yield return objInit(reader);
} while (reader.Read());
}
while (reader.NextResult()) { } // ensure any trailing
errors caught
}
}
}
}