Skip to content

Instantly share code, notes, and snippets.

@aboutdev
Created February 13, 2013 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aboutdev/4945917 to your computer and use it in GitHub Desktop.
Save aboutdev/4945917 to your computer and use it in GitHub Desktop.
Dapper change to validate mapping. DRAGONS!!!
public static Func<IDataReader, object> GetTypeDeserializer(
#if CSHARP30
Type type, IDataReader reader, int startBound, int length, bool returnNullIfFirstMissing
#else
Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false, bool validateAllFieldsHaveData = false
#endif
)
{
var dm = new DynamicMethod(string.Format("Deserialize{0}", Guid.NewGuid()), typeof(object), new[] { typeof(IDataReader) }, true);
var il = dm.GetILGenerator();
il.DeclareLocal(typeof(int));
il.DeclareLocal(type);
bool haveEnumLocal = false;
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stloc_0);
var properties = GetSettableProps(type);
var fields = GetSettableFields(type);
if (length == -1)
{
length = reader.FieldCount - startBound;
}
if (reader.FieldCount <= startBound)
{
throw new ArgumentException("When using the multi-mapping APIs ensure you set the splitOn param if you have keys other than Id", "splitOn");
}
var names = new List<string>();
for (int i = startBound; i < startBound + length; i++)
{
names.Add(reader.GetName(i));
}
var setters = (
from n in names
let prop = properties.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.Ordinal)) // property case sensitive first
?? properties.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.OrdinalIgnoreCase)) // property case insensitive second
let field = prop != null ? null : (fields.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.Ordinal)) // field case sensitive third
?? fields.FirstOrDefault(p => string.Equals(p.Name, n, StringComparison.OrdinalIgnoreCase))) // field case insensitive fourth
select new
{
Name = n,
Property = prop,
Field = field
}
).ToList();
if (validateAllFieldsHaveData)
{
var item = setters.FirstOrDefault(x => x.Field == null && x.Property == null);
if (item != null)
{
throw new MissingFieldException("Cannot map field " + item.Name);
}
}
int index = startBound;
if (type.IsValueType)
{
il.Emit(OpCodes.Ldloca_S, (byte)1);
il.Emit(OpCodes.Initobj, type);
}
else
{
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
if (ctor == null)
{
throw new InvalidOperationException("A parameterless default constructor is required to allow for dapper materialization");
}
il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Stloc_1);
}
il.BeginExceptionBlock();
if (type.IsValueType)
{
il.Emit(OpCodes.Ldloca_S, (byte)1);// [target]
}
else
{
il.Emit(OpCodes.Ldloc_1);// [target]
}
// stack is now [target]
bool first = true;
var allDone = il.DefineLabel();
foreach (var item in setters)
{
if (item.Property != null || item.Field != null)
{
il.Emit(OpCodes.Dup); // stack is now [target][target]
Label isDbNullLabel = il.DefineLabel();
Label finishLabel = il.DefineLabel();
il.Emit(OpCodes.Ldarg_0); // stack is now [target][target][reader]
EmitInt32(il, index); // stack is now [target][target][reader][index]
il.Emit(OpCodes.Dup);// stack is now [target][target][reader][index][index]
il.Emit(OpCodes.Stloc_0);// stack is now [target][target][reader][index]
il.Emit(OpCodes.Callvirt, getItem); // stack is now [target][target][value-as-object]
Type memberType = item.Property != null ? item.Property.Type : item.Field.FieldType;
if (memberType == typeof(char) || memberType == typeof(char?))
{
il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod(
memberType == typeof(char) ? "ReadChar" : "ReadNullableChar", BindingFlags.Static | BindingFlags.Public), null); // stack is now [target][target][typed-value]
}
else
{
il.Emit(OpCodes.Dup); // stack is now [target][target][value][value]
il.Emit(OpCodes.Isinst, typeof(DBNull)); // stack is now [target][target][value-as-object][DBNull or null]
il.Emit(OpCodes.Brtrue_S, isDbNullLabel); // stack is now [target][target][value-as-object]
// unbox nullable enums as the primitive, i.e. byte etc
var nullUnderlyingType = Nullable.GetUnderlyingType(memberType);
var unboxType = nullUnderlyingType != null && nullUnderlyingType.IsEnum ? nullUnderlyingType : memberType;
if (unboxType.IsEnum)
{
if (!haveEnumLocal)
{
il.DeclareLocal(typeof(string));
haveEnumLocal = true;
}
Label isNotString = il.DefineLabel();
il.Emit(OpCodes.Dup); // stack is now [target][target][value][value]
il.Emit(OpCodes.Isinst, typeof(string)); // stack is now [target][target][value-as-object][string or null]
il.Emit(OpCodes.Dup);// stack is now [target][target][value-as-object][string or null][string or null]
il.Emit(OpCodes.Stloc_2); // stack is now [target][target][value-as-object][string or null]
il.Emit(OpCodes.Brfalse_S, isNotString); // stack is now [target][target][value-as-object]
il.Emit(OpCodes.Pop); // stack is now [target][target]
il.Emit(OpCodes.Ldtoken, unboxType); // stack is now [target][target][enum-type-token]
il.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null);// stack is now [target][target][enum-type]
il.Emit(OpCodes.Ldloc_2); // stack is now [target][target][enum-type][string]
il.Emit(OpCodes.Ldc_I4_1); // stack is now [target][target][enum-type][string][true]
il.EmitCall(OpCodes.Call, enumParse, null); // stack is now [target][target][enum-as-object]
il.MarkLabel(isNotString);
il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
if (nullUnderlyingType != null)
{
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new[] { nullUnderlyingType })); // stack is now [target][target][enum-value]
}
}
else if (memberType.FullName == LinqBinary)
{
il.Emit(OpCodes.Unbox_Any, typeof(byte[])); // stack is now [target][target][byte-array]
il.Emit(OpCodes.Newobj, memberType.GetConstructor(new Type[] { typeof(byte[]) }));// stack is now [target][target][binary]
}
else
{
il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value]
}
}
// Store the value in the property/field
if (item.Property != null)
{
if (type.IsValueType)
{
il.Emit(OpCodes.Call, item.Property.Setter); // stack is now [target]
}
else
{
il.Emit(OpCodes.Callvirt, item.Property.Setter); // stack is now [target]
}
}
else
{
il.Emit(OpCodes.Stfld, item.Field); // stack is now [target]
}
il.Emit(OpCodes.Br_S, finishLabel); // stack is now [target]
il.MarkLabel(isDbNullLabel); // incoming stack: [target][target][value]
il.Emit(OpCodes.Pop); // stack is now [target][target]
il.Emit(OpCodes.Pop); // stack is now [target]
if (first && returnNullIfFirstMissing)
{
il.Emit(OpCodes.Pop);
il.Emit(OpCodes.Ldnull); // stack is now [null]
il.Emit(OpCodes.Stloc_1);
il.Emit(OpCodes.Br, allDone);
}
il.MarkLabel(finishLabel);
}
first = false;
index += 1;
}
if (type.IsValueType)
{
il.Emit(OpCodes.Pop);
}
else
{
il.Emit(OpCodes.Stloc_1); // stack is empty
}
il.MarkLabel(allDone);
il.BeginCatchBlock(typeof(Exception)); // stack is Exception
il.Emit(OpCodes.Ldloc_0); // stack is Exception, index
il.Emit(OpCodes.Ldarg_0); // stack is Exception, index, reader
il.EmitCall(OpCodes.Call, typeof(SqlMapper).GetMethod("ThrowDataException"), null);
il.EndExceptionBlock();
il.Emit(OpCodes.Ldloc_1); // stack is [rval]
if (type.IsValueType)
{
il.Emit(OpCodes.Box, type);
}
il.Emit(OpCodes.Ret);
return (Func<IDataReader, object>)dm.CreateDelegate(typeof(Func<IDataReader, object>));
}
@aboutdev
Copy link
Author

@mgravell responded to this question on http://stackoverflow.com/a/14850205/64015

Things to consider.

  1. What if the data model is outside your control. ie. It is part of a dll that you do not have access to change. In this case, you can't add a data attribute or implement an interface for it.
  2. What if I have 50 data models. What about 500. Validating these 1 by 1 by either adding attributes or implementing an interface for each one is cumbersome.
  3. By using an interface, they must be validated for every record returned. This is ok for certain scenarios but lets say i have 10K rows. In my current app I can have millions. What is the performance implications of doing that on such a large set when all I wanted to know was whether my data model maps to the columns returned from a query. The gist above shows one way of checking this scenario by ensuring that the columns returned by a query matches the data model 1 to 1. You can change it to map the model to columns if needed too and because it is evaluated only once it doesn't care if your data is 1 row or 100 million rows. Ln 50-57 is the change.
  4. I think validation of each row is also a good idea if the scenario needs it. But that is a different case to knowing if the columns will theoretically map to the data model. Potentially, the attribute example Mark provided in his response could be made to check the mapping only once by referencing them against the schema returned by the query.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment