You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

505 lines
20 KiB

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
{
/// <summary>
/// A bag of parameters that can be passed to the Dapper Query and Execute methods
/// </summary>
public partial class DynamicParameters : SqlMapper.IDynamicParameters, SqlMapper.IParameterLookup, SqlMapper.IParameterCallbacks
{
internal const DbType EnumerableMultiParameter = (DbType)(-1);
static Dictionary<SqlMapper.Identity, Action<IDbCommand, object>> paramReaderCache = new Dictionary<SqlMapper.Identity, Action<IDbCommand, object>>();
Dictionary<string, ParamInfo> parameters = new Dictionary<string, ParamInfo>();
List<object> templates;
object SqlMapper.IParameterLookup.this[string member]
{
get
{
ParamInfo param;
return parameters.TryGetValue(member, out param) ? param.Value : null;
}
}
/// <summary>
/// construct a dynamic parameter bag
/// </summary>
public DynamicParameters()
{
RemoveUnused = true;
}
/// <summary>
/// construct a dynamic parameter bag
/// </summary>
/// <param name="template">can be an anonymous type or a DynamicParameters bag</param>
public DynamicParameters(object template)
{
RemoveUnused = true;
AddDynamicParams(template);
}
/// <summary>
/// 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
/// </summary>
/// <param name="param"></param>
public void AddDynamicParams(object param)
{
var obj = param;
if (obj != null)
{
var subDynamic = obj as DynamicParameters;
if (subDynamic == null)
{
var dictionary = obj as IEnumerable<KeyValuePair<string, object>>;
if (dictionary == null)
{
templates = templates ?? new List<object>();
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<object>();
foreach (var t in subDynamic.templates)
{
templates.Add(t);
}
}
}
}
}
/// <summary>
/// Add a parameter to this dynamic parameter list
/// </summary>
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
};
}
/// <summary>
/// Add a parameter to this dynamic parameter list
/// </summary>
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);
}
/// <summary>
/// If true, the command-text is inspected and only values that are clearly used are included on the connection
/// </summary>
public bool RemoveUnused { get; set; }
/// <summary>
/// Add all the parameters needed to the command just before it executes
/// </summary>
/// <param name="command">The raw command prior to execution</param>
/// <param name="identity">Information about the query</param>
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<IDbCommand, object> 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);
}
/// <summary>
/// All the names of the param in the bag, use Get to yank them out
/// </summary>
public IEnumerable<string> ParameterNames => parameters.Select(p => p.Key);
/// <summary>
/// Get the value of a parameter
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns>The value, note DBNull.Value is not returned, instead the value is returned as null</returns>
public T Get<T>(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;
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="target">The object whose property/field you wish to populate.</param>
/// <param name="expression">A MemberExpression targeting a property/field of the target (or descendant thereof.)</param>
/// <param name="dbType"></param>
/// <param name="size">The size to set on the parameter. Defaults to 0, or DbString.DefaultLength in case of strings.</param>
/// <returns>The DynamicParameters instance</returns>
public DynamicParameters Output<T>(T target, Expression<Func<T, object>> 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<string> names = new List<string>();
List<MemberExpression> chain = new List<MemberExpression>();
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<T>.Cache;
var setter = (Action<object, DynamicParameters>)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<object, DynamicParameters>)dm.CreateDelegate(typeof(Action<object, DynamicParameters>));
lock (cache)
{
cache[lookup] = setter;
}
// Queue the preparation to be fired off when adding parameters to the DbCommand
MAKECALLBACK:
(outputCallbacks ?? (outputCallbacks = new List<Action>())).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<Action> outputCallbacks;
void SqlMapper.IParameterCallbacks.OnCompleted()
{
foreach (var param in (from p in parameters select p.Value))
{
param.OutputCallback?.Invoke(param.OutputTarget, this);
}
}
}
}