From e26ee0abe5bdff561ff59fded87cabb9f5d983a1 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Wed, 25 Jun 2014 13:18:57 +0100 Subject: [PATCH] Exploration into better support for custom type handlers --- Dapper NET40/SqlMapper.cs | 176 ++++++++++++++++++++++++++++++--- Tests/DapperTests NET40.csproj | 4 + Tests/Tests.cs | 64 ++++++++++++ 3 files changed, 230 insertions(+), 14 deletions(-) diff --git a/Dapper NET40/SqlMapper.cs b/Dapper NET40/SqlMapper.cs index d417a16..6c25ab0 100644 --- a/Dapper NET40/SqlMapper.cs +++ b/Dapper NET40/SqlMapper.cs @@ -222,6 +222,64 @@ public interface ICustomQueryParameter void AddParameter(IDbCommand command, string name); } + /// + /// Implement this interface to perform custom type-based parameter handling and value parsing + /// + public interface ITypeHandler + { + /// + /// Assign the value of a parameter before a command executes + /// + /// The parameter to configure + /// Parameter value + void SetValue(IDbDataParameter parameter, object value); + + /// + /// Parse a database value back to a typed value + /// + /// The value from the database + /// The type to parse to + /// The typed value + object Parse(Type destinationType, object value); + } + + /// + /// Base-class for simple type-handlers + /// + public abstract class TypeHandler : ITypeHandler + { + /// + /// Assign the value of a parameter before a command executes + /// + /// The parameter to configure + /// Parameter value + public abstract void SetValue(IDbDataParameter parameter, T value); + + /// + /// Parse a database value back to a typed value + /// + /// The value from the database + /// The typed value + public abstract T Parse(object value); + + void ITypeHandler.SetValue(IDbDataParameter parameter, object value) + { + if (value is DBNull) + { + parameter.Value = value; + } + else + { + SetValue(parameter, (T)value); + } + } + + object ITypeHandler.Parse(Type destinationType, object value) + { + return Parse(value); + } + } + /// /// Implement this interface to change default mapping of reader columns to type memebers /// @@ -568,10 +626,63 @@ public static void AddTypeMap(Type type, DbType dbType) typeMap[type] = dbType; } + /// + /// Configire the specified type to be processed by a custom handler + /// + public static void AddTypeHandler(Type type, ITypeHandler handler) + { + if (type == null) throw new ArgumentNullException("type"); +#pragma warning disable 618 + typeof(TypeHandlerCache<>).MakeGenericType(type).GetMethod("SetHandler", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { handler }); +#pragma warning restore 618 + if (handler == null) typeHandlers.Remove(type); + else typeHandlers[type] = handler; + + } + + /// + /// Not intended for direct usage + /// + [Obsolete("Not intended for direct usage", false)] + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static class TypeHandlerCache + { + /// + /// Not intended for direct usage + /// + [Obsolete("Not intended for direct usage", true)] + public static T Parse(object value) + { + return (T)handler.Parse(typeof(T), value); + + } + + /// + /// Not intended for direct usage + /// + [Obsolete("Not intended for direct usage", true)] + public static void SetValue(IDbDataParameter parameter, object value) + { + handler.SetValue(parameter, value); + } + + internal static void SetHandler(ITypeHandler handler) + { +#pragma warning disable 618 + TypeHandlerCache.handler = handler; +#pragma warning restore 618 + } + + private static ITypeHandler handler; + } + + private static readonly Dictionary typeHandlers = new Dictionary(); + internal const string LinqBinary = "System.Data.Linq.Binary"; - internal static DbType LookupDbType(Type type, string name) + internal static DbType LookupDbType(Type type, string name, out ITypeHandler handler) { DbType dbType; + handler = null; var nullUnderlyingType = Nullable.GetUnderlyingType(type); if (nullUnderlyingType != null) type = nullUnderlyingType; if (type.IsEnum && !typeMap.ContainsKey(type)) @@ -591,8 +702,11 @@ internal static DbType LookupDbType(Type type, string name) return DynamicParameters.EnumerableMultiParameter; } - - throw new NotSupportedException(string.Format("The member {0} of type {1} cannot be used as a parameter value", name, type)); + if (typeHandlers.TryGetValue(type, out handler)) + { + return DbType.Object; + } + throw new NotSupportedException(string.Format("The member {0} of type {1} cannot be used as a parameter value", name, type)); } @@ -2412,13 +2526,14 @@ internal static IList GetLiteralTokens(string sql) if (typeof(ICustomQueryParameter).IsAssignableFrom(prop.PropertyType)) { il.Emit(OpCodes.Ldloc_0); // stack is now [parameters] [typed-param] - il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [dbstring] - il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [dbstring] [command] - il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [dbstring] [command] [name] + il.Emit(OpCodes.Callvirt, prop.GetGetMethod()); // stack is [parameters] [custom] + il.Emit(OpCodes.Ldarg_0); // stack is now [parameters] [custom] [command] + il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [custom] [command] [name] il.EmitCall(OpCodes.Callvirt, prop.PropertyType.GetMethod("AddParameter"), null); // stack is now [parameters] continue; } - DbType dbType = LookupDbType(prop.PropertyType, prop.Name); + ITypeHandler handler; + DbType dbType = LookupDbType(prop.PropertyType, prop.Name, out handler); if (dbType == DynamicParameters.EnumerableMultiParameter) { // this actually represents special handling for list types; @@ -2452,7 +2567,7 @@ internal static IList GetLiteralTokens(string sql) il.Emit(OpCodes.Ldstr, prop.Name); // stack is now [parameters] [parameters] [parameter] [parameter] [name] il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("ParameterName").GetSetMethod(), null);// stack is now [parameters] [parameters] [parameter] } - if (dbType != DbType.Time) // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time + if (dbType != DbType.Time && handler == null) // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time { il.Emit(OpCodes.Dup);// stack is now [parameters] [[parameters]] [parameter] [parameter] EmitInt32(il, (int)dbType);// stack is now [parameters] [[parameters]] [parameter] [parameter] [db-type] @@ -2520,7 +2635,17 @@ internal static IList GetLiteralTokens(string sql) if (allDone != null) il.MarkLabel(allDone.Value); // relative stack [boxed value or DBNull] } - il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("Value").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] + + if (handler != null) + { +#pragma warning disable 618 + il.Emit(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(prop.PropertyType).GetMethod("SetValue")); // stack is now [parameters] [[parameters]] [parameter] +#pragma warning restore 618 + } + else + { + il.EmitCall(OpCodes.Callvirt, typeof(IDataParameter).GetProperty("Value").GetSetMethod(), null);// stack is now [parameters] [[parameters]] [parameter] + } if (prop.PropertyType == typeof(string)) { @@ -2739,6 +2864,15 @@ private static IDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefin return val is DBNull ? null : Enum.ToObject(effectiveType, val); }; } + ITypeHandler handler; + if(typeHandlers.TryGetValue(type, out handler)) + { + return r => + { + var val = r.GetValue(index); + return val is DBNull ? null : handler.Parse(type, val); + }; + } return r => { var val = r.GetValue(index); @@ -2980,7 +3114,16 @@ public static void SetTypeMap(Type type, ITypeMap map) TypeCode dataTypeCode = Type.GetTypeCode(dataType), unboxTypeCode = Type.GetTypeCode(unboxType); if (dataType == unboxType || dataTypeCode == unboxTypeCode || dataTypeCode == Type.GetTypeCode(nullUnderlyingType)) { - il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] + if (typeHandlers.ContainsKey(unboxType)) + { +#pragma warning disable 618 + il.EmitCall(OpCodes.Call, typeof(TypeHandlerCache<>).MakeGenericType(unboxType).GetMethod("Parse"), null); // stack is now [target][target][typed-value] +#pragma warning restore 618 + } + else + { + il.Emit(OpCodes.Unbox_Any, unboxType); // stack is now [target][target][typed-value] + } } else { @@ -3745,8 +3888,8 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) string name = Clean(param.Name); var isCustomQueryParameter = val is SqlMapper.ICustomQueryParameter; - if (dbType == null && val != null && !isCustomQueryParameter) dbType = SqlMapper.LookupDbType(val.GetType(), name); - + SqlMapper.ITypeHandler handler = null; + if (dbType == null && val != null && !isCustomQueryParameter) dbType = SqlMapper.LookupDbType(val.GetType(), name, out handler); if (dbType == DynamicParameters.EnumerableMultiParameter) { #pragma warning disable 612, 618 @@ -3771,8 +3914,13 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) { p = (IDbDataParameter)command.Parameters[name]; } - - p.Value = val ?? DBNull.Value; + if (handler == null) + { + p.Value = val ?? DBNull.Value; + } else + { + handler.SetValue(p, val ?? DBNull.Value); + } p.Direction = param.ParameterDirection; var s = val as string; if (s != null) diff --git a/Tests/DapperTests NET40.csproj b/Tests/DapperTests NET40.csproj index 5e3e6d7..f405c91 100644 --- a/Tests/DapperTests NET40.csproj +++ b/Tests/DapperTests NET40.csproj @@ -68,6 +68,10 @@ NHibernate\LinFu.DynamicProxy.dll + + False + C:\Program Files (x86)\Microsoft SQL Server\100\SDK\Assemblies\Microsoft.SqlServer.Types.dll + ..\packages\Npgsql.2.0.11\lib\Net40\Mono.Security.dll diff --git a/Tests/Tests.cs b/Tests/Tests.cs index 7e1a12b..cb610af 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -15,6 +15,8 @@ using System.Data.Common; using System.Globalization; using System.Threading; +using System.Data.Entity.Spatial; +using Microsoft.SqlServer.Types; #if POSTGRESQL using Npgsql; #endif @@ -2885,6 +2887,68 @@ public void SupportInit() obj.Flags.Equals(31); } + public void GuidIn_SO_24177902() + { + // invent and populate + Guid a = Guid.NewGuid(), b = Guid.NewGuid(), c = Guid.NewGuid(), d = Guid.NewGuid(); + connection.Execute("create table #foo (i int, g uniqueidentifier)"); + connection.Execute("insert #foo(i,g) values(@i,@g)", + new[] { new { i = 1, g = a }, new { i = 2, g = b }, + new { i = 3, g = c },new { i = 4, g = d }}); + + // check that rows 2&3 yield guids b&c + var guids = connection.Query("select g from #foo where i in (2,3)").ToArray(); + guids.Length.Equals(2); + guids.Contains(a).Equals(false); + guids.Contains(b).Equals(true); + guids.Contains(c).Equals(true); + guids.Contains(d).Equals(false); + + // in query on the guids + var rows = connection.Query("select * from #foo where g in @guids order by i", new { guids }) + .Select(row => new { i = (int)row.i, g = (Guid)row.g }).ToArray(); + rows.Length.Equals(2); + rows[0].i.Equals(2); + rows[0].g.Equals(b); + rows[1].i.Equals(3); + rows[1].g.Equals(c); + } + + class HazGeo + { + public int Id { get;set; } + public DbGeography Geo { get; set; } + } + public void DBGeography_SO24405645_SO24402424() + { + global::Dapper.SqlMapper.AddTypeHandler(typeof(DbGeography), new GeographyMapper()); + connection.Execute("create table #Geo (id int, geo geography)"); + + var obj = new HazGeo + { + Id = 1, + Geo = DbGeography.LineFromText("LINESTRING(-122.360 47.656, -122.343 47.656 )", 4326) + }; + connection.Execute("insert #Geo(id, geo) values (@Id, @Geo)", obj); + var row = connection.Query("select * from #Geo where id=1").SingleOrDefault(); + row.IsNotNull(); + row.Id.IsEqualTo(1); + row.Geo.IsNotNull(); + } + + class GeographyMapper : Dapper.SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, DbGeography value) + { + parameter.Value = value == null ? (object)DBNull.Value : (object)SqlGeography.Parse(value.AsText()); + ((SqlParameter)parameter).UdtTypeName = "GEOGRAPHY"; + } + public override DbGeography Parse(object value) + { + return (value == null || value is DBNull) ? null : DbGeography.FromText(value.ToString()); + } + } + class WithInit : ISupportInitialize { public string Value { get; set; } -- GitLab