From f52d67f180d7939f4534504d3a34ae2352939e66 Mon Sep 17 00:00:00 2001 From: Derek Date: Wed, 10 Sep 2014 00:56:27 -0500 Subject: [PATCH] Added output member expressions to DynamicParameters; included tests as well --- Dapper NET40/SqlMapper.cs | 252 ++++++++++++++++++++++++++++++++++++-- Tests/Tests.cs | 30 +++++ 2 files changed, 273 insertions(+), 9 deletions(-) diff --git a/Dapper NET40/SqlMapper.cs b/Dapper NET40/SqlMapper.cs index d1d9e21..3571aa9 100644 --- a/Dapper NET40/SqlMapper.cs +++ b/Dapper NET40/SqlMapper.cs @@ -19,6 +19,7 @@ using System.Text.RegularExpressions; using System.Diagnostics; using System.Globalization; +using System.Linq.Expressions; namespace Dapper { @@ -2456,8 +2457,8 @@ public static void PackListParameters(IDbCommand command, string namePrefix, obj listParam.Value = item ?? DBNull.Value; if (isString) { - listParam.Size = 4000; - if (item != null && ((string)item).Length > 4000) + listParam.Size = DbString.DefaultLength; + if (item != null && ((string)item).Length > DbString.DefaultLength) { listParam.Size = -1; } @@ -2851,11 +2852,11 @@ internal static IList GetLiteralTokens(string sql) { il.Emit(OpCodes.Dup); // [string] [string] il.EmitCall(OpCodes.Callvirt, typeof(string).GetProperty("Length").GetGetMethod(), null); // [string] [length] - EmitInt32(il, 4000); // [string] [length] [4000] + EmitInt32(il, DbString.DefaultLength); // [string] [length] [4000] il.Emit(OpCodes.Cgt); // [string] [0 or 1] Label isLong = il.DefineLabel(), lenDone = il.DefineLabel(); il.Emit(OpCodes.Brtrue_S, isLong); - EmitInt32(il, 4000); // [string] [4000] + EmitInt32(il, DbString.DefaultLength); // [string] [4000] il.Emit(OpCodes.Br_S, lenDone); il.MarkLabel(isLong); EmitInt32(il, -1); // [string] [-1] @@ -3027,6 +3028,11 @@ private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition comma { if (wasClosed) cnn.Close(); if (cmd != null) cmd.Dispose(); + + if (command.Parameters is DynamicParameters) + { + ((DynamicParameters)command.Parameters).FireOutputCallbacks(); + } } } @@ -3053,6 +3059,11 @@ private static T ExecuteScalarImpl(IDbConnection cnn, ref CommandDefinition c { if (wasClosed) cnn.Close(); if (cmd != null) cmd.Dispose(); + + if (command.Parameters is DynamicParameters) + { + ((DynamicParameters)command.Parameters).FireOutputCallbacks(); + } } return Parse(result); } @@ -3076,6 +3087,11 @@ private static IDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefin { if (wasClosed) cnn.Close(); if (cmd != null) cmd.Dispose(); + + if (command.Parameters is DynamicParameters) + { + ((DynamicParameters)command.Parameters).FireOutputCallbacks(); + } } } @@ -4090,6 +4106,9 @@ partial class ParamInfo public DbType? DbType { get; set; } public int? Size { get; set; } public IDbDataParameter AttachedParam { get; set; } + internal Action OutputCallback { get; set; } + internal object OutputTarget { get; set; } + internal bool CameFromTemplate { get; set; } } /// @@ -4221,6 +4240,7 @@ void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Id protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) { var literals = SqlMapper.GetLiteralTokens(identity.sql); + if (templates != null) { foreach (var template in templates) @@ -4239,9 +4259,31 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) appender(command, template); } + + // The parameters were added to the command, but not the + // DynamicParameters until now. + foreach (IDbDataParameter param in command.Parameters) + { + parameters.Add(param.ParameterName, new ParamInfo + { + AttachedParam = param, + CameFromTemplate = true, + DbType = param.DbType, + Name = param.ParameterName, + ParameterDirection = param.Direction, + Size = param.Size, + Value = param.Value + }); + } + + // Now that the parameters are added to the command, let's place our output callbacks + foreach (var generator in this.outputCallbacks) + { + generator(); + } } - foreach (var param in parameters.Values) + foreach (var param in parameters.Values.Where(p => !p.CameFromTemplate)) { var dbType = param.DbType; var val = param.Value; @@ -4286,9 +4328,9 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) var s = val as string; if (s != null) { - if (s.Length <= 4000) + if (s.Length <= DbString.DefaultLength) { - p.Size = 4000; + p.Size = DbString.DefaultLength; } } if (param.Size != null) @@ -4310,6 +4352,7 @@ protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) param.AttachedParam = p; } } + // note: most non-priveleged implementations would use: this.ReplaceLiterals(command); if(literals.Count != 0) SqlMapper.ReplaceLiterals(this, command, literals); } @@ -4345,6 +4388,190 @@ public T Get(string name) } return (T)val; } + + /// + /// Allows you to automatically populate a target property/field from output parameters. It actually + /// creates an InputOutput parameter, so you can still pass data in. + /// + /// + /// The object whose property/field you wish to populate. + /// A MemberExpression targeting a property/field of the target (or descendant thereof.) + /// + /// The size to set on the parameter. Defaults to 0, or DbString.DefaultLength in case of strings. + /// The DynamicParameters instance +#if CSHARP30 + public DynamicParameters Output(T target, Expression> expression, DbType? dbType, int? size) +#else + public DynamicParameters Output(T target, Expression> expression, DbType? dbType = null, int? size = null) +#endif + { + var failMessage = "Expression must be a property/field chain off of a(n) {0} instance"; + failMessage = string.Format(failMessage, typeof(T).Name); + Action @throw = () => { throw new InvalidOperationException(failMessage); }; + + // Is it even a MemberExpression? + var lastMemberAccess = expression.Body as MemberExpression; + + if (lastMemberAccess == null || + (lastMemberAccess.Member.MemberType != MemberTypes.Property && + lastMemberAccess.Member.MemberType != MemberTypes.Field)) + { + if (expression.Body.NodeType == ExpressionType.Convert && + expression.Body.Type == typeof(object) && + ((UnaryExpression)expression.Body).Operand is MemberExpression) + { + // It's got to be unboxed + lastMemberAccess = (MemberExpression)((UnaryExpression)expression.Body).Operand; + } + else @throw(); + } + + // Does the chain consist of MemberExpressions leading to a ParameterExpression of type T? + MemberExpression diving = lastMemberAccess; + ParameterExpression constant = null; + // Retain a list of member names and the member expressions so we can rebuild the chain. + List names = new List(); + List chain = new List(); + + do + { + // Insert the names in the right order so expression + // "Post.Author.Name" becomes parameter "PostAuthorName" + names.Insert(0, diving.Member.Name); + chain.Insert(0, diving); + + constant = diving.Expression as ParameterExpression; + diving = diving.Expression as MemberExpression; + + if (constant != null && + constant.Type == typeof(T)) + { + break; + } + else if (diving == null || + (diving.Member.MemberType != MemberTypes.Property && + diving.Member.MemberType != MemberTypes.Field)) + { + @throw(); + } + } + while (diving != null); + + var dynamicParamName = string.Join(string.Empty, names.ToArray()); + + // Before we get all emitty... + var lookup = typeof(T).Name + "_" + string.Join("|", names.ToArray()); + Action setter = null; + lock (cachedOutputSettersLock) + { + if (cachedOutputSetters.TryGetValue(lookup, out setter)) + { + goto MAKECALLBACK; + } + } + + // Come on let's build a method, let's build it, let's build it now! + var dm = new DynamicMethod(string.Format("ExpressionParam{0}", Guid.NewGuid()), null, new[] { typeof(object), this.GetType() }, true); + var il = dm.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); // [object] + il.Emit(OpCodes.Castclass, typeof(T)); // [T] + + // Count - 1 to skip the last member access + var i = 0; + for (; i < (chain.Count - 1); i++) + { + var member = chain[0].Member; + + if (member.MemberType == MemberTypes.Property) + { + var get = ((PropertyInfo)member).GetGetMethod(true); + il.Emit(OpCodes.Callvirt, get); // [Member{i}] + } + else // Else it must be a field! + { + il.Emit(OpCodes.Ldfld, ((FieldInfo)member)); // [Member{i}] + } + } + + var paramGetter = this.GetType().GetMethod("Get", new Type[] { typeof(string) }).MakeGenericMethod(lastMemberAccess.Type); + + il.Emit(OpCodes.Ldarg_1); // [target] [DynamicParameters] + il.Emit(OpCodes.Ldstr, dynamicParamName); // [target] [DynamicParameters] [ParamName] + il.Emit(OpCodes.Callvirt, paramGetter); // [target] [value], it's already typed thanks to generic method + + // GET READY + var lastMember = lastMemberAccess.Member; + if (lastMember.MemberType == MemberTypes.Property) + { + var set = ((PropertyInfo)lastMember).GetSetMethod(true); + il.Emit(OpCodes.Callvirt, set); // SET + } + else + { + il.Emit(OpCodes.Stfld, ((FieldInfo)lastMember)); // SET + } + + il.Emit(OpCodes.Ret); // GO + + setter = (Action)dm.CreateDelegate(typeof(Action)); + lock (cachedOutputSettersLock) + { + if (!cachedOutputSetters.ContainsKey(lookup)) + { + cachedOutputSetters.Add(lookup, setter); + } + } + + // Queue the preparation to be fired off when adding parameters to the DbCommand + MAKECALLBACK: + this.outputCallbacks.Add(() => + { + // Finally, prep the parameter and attach the callback to it + ParamInfo parameter; + var targetMemberType = lastMemberAccess.Type; + int sizeToSet = (!size.HasValue && targetMemberType == typeof(string)) ? DbString.DefaultLength : size ?? 0; + + if (this.parameters.TryGetValue(dynamicParamName, out parameter)) + { + parameter.ParameterDirection = parameter.AttachedParam.Direction = ParameterDirection.InputOutput; + + if (parameter.AttachedParam.Size == 0) + { + parameter.Size = parameter.AttachedParam.Size = sizeToSet; + } + } + else + { + SqlMapper.ITypeHandler handler; + dbType = (!dbType.HasValue) ? SqlMapper.LookupDbType(targetMemberType, targetMemberType.Name, out handler) : dbType; + + // CameFromTemplate property would not apply here because this new param + // Still needs to be added to the command + this.Add(dynamicParamName, expression.Compile().Invoke(target), null, ParameterDirection.InputOutput, sizeToSet); + } + + parameter = this.parameters[dynamicParamName]; + parameter.OutputCallback = setter; + parameter.OutputTarget = target; + }); + + return this; + } + + private readonly List outputCallbacks = new List(); + + private readonly Dictionary> cachedOutputSetters = new Dictionary>(); + + private readonly object cachedOutputSettersLock = new object(); + + internal void FireOutputCallbacks() + { + foreach (var param in (from p in parameters select p.Value)) + { + if (param.OutputCallback != null) param.OutputCallback(param.OutputTarget, this); + } + } } sealed class DataTableHandler : Dapper.SqlMapper.ITypeHandler @@ -4420,6 +4647,13 @@ internal static void Set(IDbDataParameter parameter, DataTable table, string typ /// sealed partial class DbString : Dapper.SqlMapper.ICustomQueryParameter { + /// + /// A value to set the default value of strings + /// going through Dapper. Default is 4000, any value larger than this + /// field will not have the default value applied. + /// + public const int DefaultLength = 4000; + /// /// Create a new DbString /// @@ -4454,9 +4688,9 @@ public void AddParameter(IDbCommand command, string name) var param = command.CreateParameter(); param.ParameterName = name; param.Value = (object)Value ?? DBNull.Value; - if (Length == -1 && Value != null && Value.Length <= 4000) + if (Length == -1 && Value != null && Value.Length <= DefaultLength) { - param.Size = 4000; + param.Size = DefaultLength; } else { diff --git a/Tests/Tests.cs b/Tests/Tests.cs index eceffca..489762a 100644 --- a/Tests/Tests.cs +++ b/Tests/Tests.cs @@ -1175,6 +1175,33 @@ public void TestSupportForDynamicParameters() p.Get("age").IsEqualTo(11); } + + [ActiveTest] + public void TestSupportForDynamicParametersOutputExpressions() + { + var bob = new Person { Name = "bob", PersonId = 1, Address = new Address { PersonId = 2 } }; + + var p = new DynamicParameters(bob); + p.Output(bob, b => b.PersonId); + p.Output(bob, b => b.Occupation); + p.Output(bob, b => b.NumberOfLegs); + p.Output(bob, b => b.Address.Name); + p.Output(bob, b => b.Address.PersonId); + + connection.Execute(@" +SET @Occupation = 'grillmaster' +SET @PersonId = @PersonId + 1 +SET @NumberOfLegs = @NumberOfLegs - 1 +SET @AddressName = 'bobs burgers' +SET @AddressPersonId = @PersonId", p); + + bob.Occupation.IsEqualTo("grillmaster"); + bob.PersonId.IsEqualTo(2); + bob.NumberOfLegs.IsEqualTo(1); + bob.Address.Name.IsEqualTo("bobs burgers"); + bob.Address.PersonId.IsEqualTo(2); + } + public void TestSupportForExpandoObjectParameters() { dynamic p = new ExpandoObject(); @@ -1231,6 +1258,9 @@ class Person { public int PersonId { get; set; } public string Name { get; set; } + public string Occupation { get; private set; } + public int NumberOfLegs = 2; + public Address Address { get; set; } } class Address -- GitLab