using System; using System.Collections.Generic; using System.Data; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; #if NETCOREAPP using ApplicationException = System.InvalidOperationException; #endif namespace Dapper { /// /// A bag of parameters that can be passed to the Dapper Query and Execute methods /// public partial class DynamicParameters : SqlMapper.IDynamicParameters, SqlMapper.IParameterLookup, SqlMapper.IParameterCallbacks { internal const DbType EnumerableMultiParameter = (DbType)(-1); static Dictionary> paramReaderCache = new Dictionary>(); Dictionary parameters = new Dictionary(); List templates; object SqlMapper.IParameterLookup.this[string member] { get { ParamInfo param; return parameters.TryGetValue(member, out param) ? param.Value : null; } } /// /// construct a dynamic parameter bag /// public DynamicParameters() { RemoveUnused = true; } /// /// construct a dynamic parameter bag /// /// can be an anonymous type or a DynamicParameters bag public DynamicParameters(object template) { RemoveUnused = true; AddDynamicParams(template); } /// /// Append a whole object full of params to the dynamic /// EG: AddDynamicParams(new {A = 1, B = 2}) // will add property A and B to the dynamic /// /// public void AddDynamicParams(object param) { var obj = param; if (obj != null) { var subDynamic = obj as DynamicParameters; if (subDynamic == null) { var dictionary = obj as IEnumerable>; if (dictionary == null) { templates = templates ?? new List(); templates.Add(obj); } else { foreach (var kvp in dictionary) { Add(kvp.Key, kvp.Value, null, null, null); } } } else { if (subDynamic.parameters != null) { foreach (var kvp in subDynamic.parameters) { parameters.Add(kvp.Key, kvp.Value); } } if (subDynamic.templates != null) { templates = templates ?? new List(); foreach (var t in subDynamic.templates) { templates.Add(t); } } } } } /// /// Add a parameter to this dynamic parameter list /// public void Add(string name, object value, DbType? dbType, ParameterDirection? direction, int? size) { parameters[Clean(name)] = new ParamInfo { Name = name, Value = value, ParameterDirection = direction ?? ParameterDirection.Input, DbType = dbType, Size = size }; } /// /// Add a parameter to this dynamic parameter list /// public void Add( string name, object value = null, DbType? dbType = null, ParameterDirection? direction = null, int? size = null, byte? precision = null, byte? scale = null ) { parameters[Clean(name)] = new ParamInfo { Name = name, Value = value, ParameterDirection = direction ?? ParameterDirection.Input, DbType = dbType, Size = size, Precision = precision, Scale = scale }; } static string Clean(string name) { if (!string.IsNullOrEmpty(name)) { switch (name[0]) { case '@': case ':': case '?': return name.Substring(1); } } return name; } void SqlMapper.IDynamicParameters.AddParameters(IDbCommand command, SqlMapper.Identity identity) { AddParameters(command, identity); } /// /// If true, the command-text is inspected and only values that are clearly used are included on the connection /// public bool RemoveUnused { get; set; } /// /// Add all the parameters needed to the command just before it executes /// /// The raw command prior to execution /// Information about the query protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) { var literals = SqlMapper.GetLiteralTokens(identity.sql); if (templates != null) { foreach (var template in templates) { var newIdent = identity.ForDynamicParameters(template.GetType()); Action appender; lock (paramReaderCache) { if (!paramReaderCache.TryGetValue(newIdent, out appender)) { appender = SqlMapper.CreateParamInfoGenerator(newIdent, true, RemoveUnused, literals); paramReaderCache[newIdent] = appender; } } appender(command, template); } // The parameters were added to the command, but not the // DynamicParameters until now. foreach (IDbDataParameter param in command.Parameters) { // If someone makes a DynamicParameters with a template, // then explicitly adds a parameter of a matching name, // it will already exist in 'parameters'. if (!parameters.ContainsKey(param.ParameterName)) { 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 var tmp = outputCallbacks; if (tmp != null) { foreach (var generator in tmp) { generator(); } } } foreach (var param in parameters.Values) { if (param.CameFromTemplate) continue; var dbType = param.DbType; var val = param.Value; string name = Clean(param.Name); var isCustomQueryParameter = val is SqlMapper.ICustomQueryParameter; SqlMapper.ITypeHandler handler = null; if (dbType == null && val != null && !isCustomQueryParameter) { #pragma warning disable 618 dbType = SqlMapper.LookupDbType(val.GetType(), name, true, out handler); #pragma warning disable 618 } if (isCustomQueryParameter) { ((SqlMapper.ICustomQueryParameter)val).AddParameter(command, name); } else if (dbType == EnumerableMultiParameter) { #pragma warning disable 612, 618 SqlMapper.PackListParameters(command, name, val); #pragma warning restore 612, 618 } else { bool add = !command.Parameters.Contains(name); IDbDataParameter p; if (add) { p = command.CreateParameter(); p.ParameterName = name; } else { p = (IDbDataParameter)command.Parameters[name]; } p.Direction = param.ParameterDirection; if (handler == null) { #pragma warning disable 0618 p.Value = SqlMapper.SanitizeParameterValue(val); #pragma warning restore 0618 if (dbType != null && p.DbType != dbType) { p.DbType = dbType.Value; } var s = val as string; if (s?.Length <= DbString.DefaultLength) { p.Size = DbString.DefaultLength; } if (param.Size != null) p.Size = param.Size.Value; if (param.Precision != null) p.Precision = param.Precision.Value; if (param.Scale != null) p.Scale = param.Scale.Value; } else { if (dbType != null) p.DbType = dbType.Value; if (param.Size != null) p.Size = param.Size.Value; if (param.Precision != null) p.Precision = param.Precision.Value; if (param.Scale != null) p.Scale = param.Scale.Value; handler.SetValue(p, val ?? DBNull.Value); } if (add) { command.Parameters.Add(p); } param.AttachedParam = p; } } // note: most non-priveleged implementations would use: this.ReplaceLiterals(command); if (literals.Count != 0) SqlMapper.ReplaceLiterals(this, command, literals); } /// /// All the names of the param in the bag, use Get to yank them out /// public IEnumerable ParameterNames => parameters.Select(p => p.Key); /// /// Get the value of a parameter /// /// /// /// The value, note DBNull.Value is not returned, instead the value is returned as null public T Get(string name) { var paramInfo = parameters[Clean(name)]; var attachedParam = paramInfo.AttachedParam; object val = attachedParam == null ? paramInfo.Value : attachedParam.Value; if (val == DBNull.Value) { if (default(T) != null) { throw new ApplicationException("Attempting to cast a DBNull to a non nullable type! Note that out/return parameters will not have updated values until the data stream completes (after the 'foreach' for Query(..., buffered: false), or after the GridReader has been disposed for QueryMultiple)"); } return default(T); } 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 public DynamicParameters Output(T target, Expression> expression, DbType? dbType = null, int? size = null) { 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 is PropertyInfo) && !(lastMemberAccess.Member is FieldInfo))) { 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; // 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); var constant = diving?.Expression as ParameterExpression; diving = diving?.Expression as MemberExpression; if (constant != null && constant.Type == typeof(T)) { break; } else if (diving == null || (!(diving.Member is PropertyInfo) && !(diving.Member is FieldInfo))) { @throw(); } } while (diving != null); var dynamicParamName = string.Join(string.Empty, names.ToArray()); // Before we get all emitty... var lookup = string.Join("|", names.ToArray()); var cache = CachedOutputSetters.Cache; var setter = (Action)cache[lookup]; if (setter != null) goto MAKECALLBACK; // Come on let's build a method, let's build it, let's build it now! var dm = new DynamicMethod("ExpressionParam" + Guid.NewGuid().ToString(), null, new[] { typeof(object), 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 is PropertyInfo) { 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 = 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 is PropertyInfo) { 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 (cache) { cache[lookup] = setter; } // Queue the preparation to be fired off when adding parameters to the DbCommand MAKECALLBACK: (outputCallbacks ?? (outputCallbacks = new List())).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 (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) #pragma warning disable 618 ? SqlMapper.LookupDbType(targetMemberType, targetMemberType?.Name, true, out handler) #pragma warning restore 618 : dbType; // CameFromTemplate property would not apply here because this new param // Still needs to be added to the command Add(dynamicParamName, expression.Compile().Invoke(target), null, ParameterDirection.InputOutput, sizeToSet); } parameter = parameters[dynamicParamName]; parameter.OutputCallback = setter; parameter.OutputTarget = target; }); return this; } private List outputCallbacks; void SqlMapper.IParameterCallbacks.OnCompleted() { foreach (var param in (from p in parameters select p.Value)) { param.OutputCallback?.Invoke(param.OutputTarget, this); } } } }