Add object-based stored procedure parameter binding

This commit is contained in:
2026-02-27 14:26:16 +01:00
parent 86c388dcdb
commit 79cce3d1f4
7 changed files with 1017 additions and 282 deletions

View File

@@ -5377,133 +5377,110 @@ namespace DynamORM
if (alen > 0)
{
for (int i = 0; i < alen; i++)
if (alen == 1 && DynamicProcedureParameterBinder.CanBind(args[0]))
{
object arg = args[i];
if (arg is DynamicExpando)
cmd.AddParameters(_db, (DynamicExpando)arg);
else if (arg is ExpandoObject)
cmd.AddParameters(_db, (ExpandoObject)arg);
else if (arg is DynamicColumn)
DynamicProcedureParameterBinder.BindingResult bindingResult = DynamicProcedureParameterBinder.Bind(_db, cmd, args[0]);
retParams = bindingResult.ReturnParameters;
retIsAdded = bindingResult.ReturnValueAdded;
}
else
{
for (int i = 0; i < alen; i++)
{
var dcv = (DynamicColumn)arg;
object arg = args[i];
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
if (info.ArgumentNames.Count > i)
if (arg is DynamicExpando)
cmd.AddParameters(_db, (DynamicExpando)arg);
else if (arg is ExpandoObject)
cmd.AddParameters(_db, (ExpandoObject)arg);
else if (arg is DynamicColumn)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
var dcv = (DynamicColumn)arg;
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
if (info.ArgumentNames.Count > i)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
}
paramName = dcv.Alias ?? dcv.ColumnName ??
(dcv.Schema.HasValue ? dcv.Schema.Value.Name : null) ??
paramName;
if (!isOut && !isRet && !isBoth)
{
isOut = dcv.ParameterDirection == ParameterDirection.Output;
isRet = dcv.ParameterDirection == ParameterDirection.ReturnValue;
isBoth = dcv.ParameterDirection == ParameterDirection.InputOutput;
}
if (isRet)
retIsAdded = true;
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
if (dcv.Schema != null)
{
var ds = dcv.Schema.Value;
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
ds.Type, ds.Size, ds.Precision, ds.Scale,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
else
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
paramName = dcv.Alias ?? dcv.ColumnName ??
(dcv.Schema.HasValue ? dcv.Schema.Value.Name : null) ??
paramName;
if (!isOut && !isRet && !isBoth)
else if (arg is DynamicSchemaColumn)
{
isOut = dcv.ParameterDirection == ParameterDirection.Output;
isRet = dcv.ParameterDirection == ParameterDirection.ReturnValue;
isBoth = dcv.ParameterDirection == ParameterDirection.InputOutput;
}
if (isRet)
retIsAdded = true;
var dsc = (DynamicSchemaColumn)arg;
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
if (dcv.Schema != null)
{
var ds = dcv.Schema.Value;
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
ds.Type, ds.Size, ds.Precision, ds.Scale,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
else
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
else if (arg is DynamicSchemaColumn)
{
var dsc = (DynamicSchemaColumn)arg;
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
if (info.ArgumentNames.Count > i)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
if (info.ArgumentNames.Count > i)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
}
paramName = dsc.Name ?? paramName;
if (isRet)
retIsAdded = true;
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
dsc.Type, dsc.Size, dsc.Precision, dsc.Scale,
DBNull.Value);
}
else
{
if (info.ArgumentNames.Count > i && !string.IsNullOrEmpty(info.ArgumentNames[i]))
{
bool isOut = info.ArgumentNames[i].StartsWith("out_");
bool isRet = info.ArgumentNames[i].StartsWith("ret_");
bool isBoth = info.ArgumentNames[i].StartsWith("both_");
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
}
paramName = dsc.Name ?? paramName;
if (isRet)
retIsAdded = true;
string paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
if (isOut || isBoth || isRet)
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
@@ -5515,12 +5492,44 @@ namespace DynamORM
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? isRet ? DbType.Int32 : DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : arg);
dsc.Type, dsc.Size, dsc.Precision, dsc.Scale,
DBNull.Value);
}
else
cmd.AddParameter(_db, arg);
{
if (info.ArgumentNames.Count > i && !string.IsNullOrEmpty(info.ArgumentNames[i]))
{
bool isOut = info.ArgumentNames[i].StartsWith("out_");
bool isRet = info.ArgumentNames[i].StartsWith("ret_");
bool isBoth = info.ArgumentNames[i].StartsWith("both_");
if (isRet)
retIsAdded = true;
string paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
if (isOut || isBoth || isRet)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? isRet ? DbType.Int32 : DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : arg);
}
else
cmd.AddParameter(_db, arg);
}
}
}
}
@@ -6826,6 +6835,54 @@ namespace DynamORM
#endregion IExtendedDisposable Members
}
/// <summary>Declares metadata for object-based stored procedure parameters.</summary>
[AttributeUsage(AttributeTargets.Property)]
public class ProcedureParameterAttribute : ColumnAttribute
{
/// <summary>Sentinel used when database type was not provided.</summary>
public const int UnspecifiedDbType = -1;
/// <summary>Sentinel used when size was not provided.</summary>
public const int UnspecifiedSize = -1;
/// <summary>Sentinel used when precision or scale was not provided.</summary>
public const byte UnspecifiedByte = byte.MaxValue;
/// <summary>Gets or sets parameter direction. Defaults to input.</summary>
public ParameterDirection Direction { get; set; }
/// <summary>Gets or sets explicit parameter order. Lower values are emitted first.</summary>
public int Order { get; set; }
/// <summary>Gets or sets parameter database type.</summary>
public DbType DbType { get; set; }
/// <summary>Gets or sets parameter size.</summary>
public new int Size { get; set; }
/// <summary>Gets or sets parameter precision.</summary>
public new byte Precision { get; set; }
/// <summary>Gets or sets parameter scale.</summary>
public new byte Scale { get; set; }
/// <summary>Initializes a new instance of the <see cref="ProcedureParameterAttribute"/> class.</summary>
public ProcedureParameterAttribute()
{
Direction = ParameterDirection.Input;
Order = int.MaxValue;
DbType = (DbType)UnspecifiedDbType;
Size = UnspecifiedSize;
Precision = UnspecifiedByte;
Scale = UnspecifiedByte;
}
/// <summary>Initializes a new instance of the <see cref="ProcedureParameterAttribute"/> class.</summary>
public ProcedureParameterAttribute(string name)
: this()
{
Name = name;
}
}
namespace Builders
{
/// <summary>Typed join kind used by typed fluent builder APIs.</summary>
@@ -14416,6 +14473,133 @@ namespace DynamORM
return resultTable;
}
}
internal static class DynamicProcedureParameterBinder
{
internal sealed class BindingResult
{
public bool ReturnValueAdded { get; set; }
public Dictionary<string, int> ReturnParameters { get; set; }
}
internal static bool CanBind(object item)
{
if (item == null)
return false;
Type type = item.GetType();
if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(Guid) || type == typeof(TimeSpan))
return false;
return GetBindableProperties(type).Any();
}
internal static BindingResult Bind(DynamicDatabase db, IDbCommand cmd, object item)
{
if (db == null)
throw new ArgumentNullException("db");
if (cmd == null)
throw new ArgumentNullException("cmd");
if (item == null)
throw new ArgumentNullException("item");
BindingResult result = new BindingResult();
foreach (PropertyInfo property in GetBindableProperties(item.GetType()))
{
ProcedureParameterAttribute procAttr = property.GetCustomAttributes(typeof(ProcedureParameterAttribute), true).Cast<ProcedureParameterAttribute>().FirstOrDefault();
ColumnAttribute colAttr = property.GetCustomAttributes(typeof(ColumnAttribute), true).Cast<ColumnAttribute>().FirstOrDefault();
string name = (procAttr != null && !string.IsNullOrEmpty(procAttr.Name) ? procAttr.Name : null)
?? (colAttr != null && !string.IsNullOrEmpty(colAttr.Name) ? colAttr.Name : null)
?? property.Name;
ParameterDirection direction = procAttr == null ? ParameterDirection.Input : procAttr.Direction;
object value = property.GetValue(item, null);
DbType dbType = ResolveDbType(property.PropertyType, value, procAttr, colAttr, direction);
int size = ResolveSize(dbType, value, procAttr, colAttr, direction);
byte precision = (procAttr != null && procAttr.Precision != ProcedureParameterAttribute.UnspecifiedByte ? procAttr.Precision : default(byte));
byte scale = (procAttr != null && procAttr.Scale != ProcedureParameterAttribute.UnspecifiedByte ? procAttr.Scale : default(byte));
if (procAttr == null && colAttr != null)
{
precision = colAttr.Precision ?? precision;
scale = colAttr.Scale ?? scale;
}
if (direction == ParameterDirection.ReturnValue)
result.ReturnValueAdded = true;
if (direction == ParameterDirection.Output || direction == ParameterDirection.InputOutput || direction == ParameterDirection.ReturnValue)
{
if (result.ReturnParameters == null)
result.ReturnParameters = new Dictionary<string, int>();
result.ReturnParameters.Add(name, cmd.Parameters.Count);
}
cmd.AddParameter(
db.GetParameterName(name),
direction,
dbType,
size,
precision,
scale,
direction == ParameterDirection.Output || direction == ParameterDirection.ReturnValue ? DBNull.Value : (value ?? DBNull.Value));
}
return result;
}
private static IEnumerable<PropertyInfo> GetBindableProperties(Type type)
{
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanRead && x.GetIndexParameters().Length == 0)
.Where(x => x.GetCustomAttributes(typeof(ProcedureParameterAttribute), true).Any() || x.GetCustomAttributes(typeof(ColumnAttribute), true).Any())
.OrderBy(x =>
{
ProcedureParameterAttribute attr = x.GetCustomAttributes(typeof(ProcedureParameterAttribute), true).Cast<ProcedureParameterAttribute>().FirstOrDefault();
return attr == null ? int.MaxValue : attr.Order;
})
.ThenBy(x => x.MetadataToken);
}
private static DbType ResolveDbType(Type propertyType, object value, ProcedureParameterAttribute procAttr, ColumnAttribute colAttr, ParameterDirection direction)
{
if (procAttr != null && (int)procAttr.DbType != ProcedureParameterAttribute.UnspecifiedDbType)
return procAttr.DbType;
if (colAttr != null && colAttr.Type.HasValue)
return colAttr.Type.Value;
Type targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
if (targetType == typeof(object) && value != null)
targetType = value.GetType();
if (value == null && direction == ParameterDirection.ReturnValue)
return DbType.Int32;
return targetType.ToDbType();
}
private static int ResolveSize(DbType dbType, object value, ProcedureParameterAttribute procAttr, ColumnAttribute colAttr, ParameterDirection direction)
{
if (procAttr != null && procAttr.Size != ProcedureParameterAttribute.UnspecifiedSize)
return procAttr.Size;
if (colAttr != null && colAttr.Size.HasValue)
return colAttr.Size.Value;
if (direction == ParameterDirection.ReturnValue)
return 4;
if (dbType == DbType.AnsiString || dbType == DbType.AnsiStringFixedLength)
{
if (value != null)
return value.ToString().Length > 8000 ? -1 : 8000;
return 8000;
}
if (dbType == DbType.String || dbType == DbType.StringFixedLength)
{
if (value != null)
return value.ToString().Length > 4000 ? -1 : 4000;
return 4000;
}
return 0;
}
}
/// <summary>Framework detection and specific implementations.</summary>
public static class FrameworkTools
{