Files
Dapper/Dapper/DynamicParameters.cs
Marc Gravell ddc6737113 - prefer dictionary over hashset, to avoid additional lookup
- defer the cost so we only start paying it for non-trivial commands
2024-03-07 12:51:59 +00:00

531 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
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);
private static readonly Dictionary<SqlMapper.Identity, Action<IDbCommand, object>> paramReaderCache = new Dictionary<SqlMapper.Identity, Action<IDbCommand, object>>();
private readonly Dictionary<string, ParamInfo> parameters = new Dictionary<string, ParamInfo>();
private List<object>? templates;
object? SqlMapper.IParameterLookup.this[string name] =>
parameters.TryGetValue(name, out ParamInfo? 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 is not null)
{
if (obj is DynamicParameters subDynamic)
{
if (subDynamic.parameters is not null)
{
foreach (var kvp in subDynamic.parameters)
{
parameters.Add(kvp.Key, kvp.Value);
}
}
if (subDynamic.templates is not null)
{
templates ??= new List<object>();
foreach (var t in subDynamic.templates)
{
templates.Add(t);
}
}
}
else
{
if (obj is IEnumerable<KeyValuePair<string, object>> dictionary)
{
foreach (var kvp in dictionary)
{
Add(kvp.Key, kvp.Value, null, null, null);
}
}
else
{
templates ??= new List<object>();
templates.Add(obj);
}
}
}
}
/// <summary>
/// Add a parameter to this dynamic parameter list.
/// </summary>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The value of the parameter.</param>
/// <param name="dbType">The type of the parameter.</param>
/// <param name="direction">The in or out direction of the parameter.</param>
/// <param name="size">The size of the parameter.</param>
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>
/// <param name="name">The name of the parameter.</param>
/// <param name="value">The value of the parameter.</param>
/// <param name="dbType">The type of the parameter.</param>
/// <param name="direction">The in or out direction of the parameter.</param>
/// <param name="size">The size of the parameter.</param>
/// <param name="precision">The precision of the parameter.</param>
/// <param name="scale">The scale of the parameter.</param>
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
};
}
private 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; }
internal static bool ShouldSetDbType(DbType? dbType)
=> dbType.HasValue && dbType.GetValueOrDefault() != EnumerableMultiParameter;
internal static bool ShouldSetDbType(DbType dbType)
=> dbType != EnumerableMultiParameter; // just in case called with non-nullable
/// <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 is not 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 is not null)
{
foreach (var generator in tmp)
{
generator();
}
}
}
static IDbDataParameter GetOrAdd(IDbCommand command, string name, ref Dictionary<string, IDbDataParameter>? lookup)
{
IDbDataParameter p;
if (lookup is null && command.Parameters.Count <= 10)
{
// don't pay dictionary overhead for small collections
if (command.Parameters.Contains(name))
{
// already exists
p = (IDbDataParameter)command.Parameters[name];
}
else
{
// new
p = command.CreateParameter();
p.ParameterName = name;
command.Parameters.Add(p);
}
}
else
{
if (lookup is null)
{
// build the initial map
lookup = new Dictionary<string, IDbDataParameter>(command.Parameters.Count + 10, StringComparer.CurrentCultureIgnoreCase);
foreach (IDbDataParameter existing in command.Parameters)
{
lookup[existing.ParameterName] = existing;
}
}
// add if necessary
if (!lookup.TryGetValue(name, out p!))
{
p = command.CreateParameter();
p.ParameterName = name;
lookup.Add(name, p);
command.Parameters.Add(p);
}
}
return p;
}
Dictionary<string, IDbDataParameter>? paramLookup = null;
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 is null && val is not 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
{
var p = GetOrAdd(command, name, ref paramLookup);
p.Direction = param.ParameterDirection;
if (handler is null)
{
#pragma warning disable 0618
p.Value = SqlMapper.SanitizeParameterValue(val);
#pragma warning restore 0618
if (ShouldSetDbType(dbType) && p.DbType != dbType.GetValueOrDefault())
{
p.DbType = dbType.GetValueOrDefault();
}
var s = val as string;
if (s?.Length <= DbString.DefaultLength)
{
p.Size = DbString.DefaultLength;
}
if (param.Size is not null) p.Size = param.Size.Value;
if (param.Precision is not null) p.Precision = param.Precision.Value;
if (param.Scale is not null) p.Scale = param.Scale.Value;
}
else
{
if (ShouldSetDbType(dbType)) p.DbType = dbType.GetValueOrDefault();
if (param.Size is not null) p.Size = param.Size.Value;
if (param.Precision is not null) p.Precision = param.Precision.Value;
if (param.Scale is not null) p.Scale = param.Scale.Value;
handler.SetValue(p, val ?? DBNull.Value);
}
param.AttachedParam = p;
}
}
// note: most non-privileged 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 is null ? paramInfo.Value : attachedParam.Value;
if (val == DBNull.Value)
{
if (default(T) is not 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!;
}
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)
{
static void ThrowInvalidChain()
=> throw new InvalidOperationException($"Expression must be a property/field chain off of a(n) {typeof(T).Name} instance");
// Is it even a MemberExpression?
#pragma warning disable IDE0019 // Use pattern matching - already complex enough
var lastMemberAccess = expression.Body as MemberExpression;
#pragma warning restore IDE0019 // Use pattern matching
if (lastMemberAccess is 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 member)
{
// It's got to be unboxed
lastMemberAccess = member;
}
else
{
ThrowInvalidChain();
}
}
// 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);
#pragma warning disable IDE0019 // use pattern matching; this is fine!
var constant = diving?.Expression as ParameterExpression;
diving = diving?.Expression as MemberExpression;
#pragma warning restore IDE0019 // use pattern matching
if (constant is not null && constant.Type == typeof(T))
{
break;
}
else if (diving is null
|| (!(diving.Member is PropertyInfo)
&& !(diving.Member is FieldInfo)))
{
ThrowInvalidChain();
}
}
while (diving is not null);
var dynamicParamName = string.Concat(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 is not 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
for (var i = 0; i < chain.Count - 1; i++)
{
var member = chain[i]?.Member;
if (member is PropertyInfo info)
{
var get = info.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 property)
{
var set = property.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 ??= new List<Action>()).Add(() =>
{
// Finally, prep the parameter and attach the callback to it
var targetMemberType = lastMemberAccess?.Type;
int sizeToSet = (!size.HasValue && targetMemberType == typeof(string)) ? DbString.DefaultLength : size ?? 0;
if (parameters.TryGetValue(dynamicParamName, out ParamInfo? parameter))
{
parameter.ParameterDirection = parameter.AttachedParam.Direction = ParameterDirection.InputOutput;
if (parameter.AttachedParam.Size == 0)
{
parameter.Size = parameter.AttachedParam.Size = sizeToSet;
}
}
else
{
// CameFromTemplate property would not apply here because this new param
// Still needs to be added to the command
Add(dynamicParamName, expression.Compile().Invoke(target), dbType, 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);
}
}
}
}