Add typed procedure descriptors and Exec invoker

This commit is contained in:
2026-02-27 16:27:49 +01:00
parent 9ce10273f1
commit 416404f8d1
7 changed files with 410 additions and 23 deletions

View File

@@ -5361,22 +5361,44 @@ namespace DynamORM
// Get generic types
IList<Type> types = binder.GetGenericTypeArguments() ?? new List<Type>();
Type declaredResultType = null;
string procedureName = binder.Name;
string resultName = binder.Name;
Type execArgumentsType = null;
if (binder.Name == "Exec")
{
if (types.Count != 1)
throw new InvalidOperationException("Exec<TProcedure>(args) requires exactly one generic procedure descriptor type.");
DynamicProcedureDescriptor descriptor = DynamicProcedureDescriptor.Resolve(types[0]);
procedureName = descriptor.ProcedureName;
resultName = descriptor.ResultName;
execArgumentsType = descriptor.ArgumentsType;
declaredResultType = descriptor.ResultType;
types = new List<Type>();
if (args.Length > 1)
throw new InvalidOperationException("Exec<TProcedure>(args) accepts at most one arguments contract instance.");
if (args.Length == 1 && args[0] != null && !execArgumentsType.IsAssignableFrom(args[0].GetType()))
throw new InvalidOperationException(string.Format("Exec<{0}>(args) expects argument of type '{1}', received '{2}'.", descriptor.ProcedureType.FullName, execArgumentsType.FullName, args[0].GetType().FullName));
}
Dictionary<string, int> retParams = null;
using (IDbConnection con = _db.Open())
using (IDbCommand cmd = con.CreateCommand())
{
if (_prefixes == null || _prefixes.Count == 0)
cmd.SetCommand(CommandType.StoredProcedure, binder.Name);
cmd.SetCommand(CommandType.StoredProcedure, procedureName);
else
cmd.SetCommand(CommandType.StoredProcedure, string.Format("{0}.{1}", string.Join(".", _prefixes), binder.Name));
cmd.SetCommand(CommandType.StoredProcedure, string.Format("{0}.{1}", string.Join(".", _prefixes), procedureName));
#region Prepare arguments
int alen = args.Length;
bool retIsAdded = false;
declaredResultType = alen == 1 ? DynamicProcedureResultBinder.GetDeclaredResultType(args[0]) : null;
if (declaredResultType == null)
declaredResultType = alen == 1 ? DynamicProcedureResultBinder.GetDeclaredResultType(args[0]) : null;
if (alen > 0)
{
@@ -5561,7 +5583,7 @@ namespace DynamORM
{
using (IDataReader rdr = cmd.ExecuteReader())
using (IDataReader cache = rdr.CachedReader())
mainResult = cache.ToDataTable(binder.Name);
mainResult = cache.ToDataTable(resultName);
}
else if (types[0].IsGenericEnumerable())
{
@@ -5688,9 +5710,9 @@ namespace DynamORM
if (mainResult != null)
{
if (mainResult == DBNull.Value)
res.Add(binder.Name, null);
res.Add(resultName, null);
else
res.Add(binder.Name, mainResult);
res.Add(resultName, mainResult);
}
foreach (KeyValuePair<string, int> pos in retParams)
res.Add(pos.Key, ((IDbDataParameter)cmd.Parameters[pos.Value]).Value);
@@ -5705,14 +5727,14 @@ namespace DynamORM
result = res.ToDynamic();
}
else if (declaredResultType != null)
result = DynamicProcedureResultBinder.BindPayload(declaredResultType, binder.Name, mainResult, res.Where(x => x.Key != binder.Name).ToDictionary(x => x.Key, x => x.Value), mainResult != null && declaredResultType.IsInstanceOfType(mainResult) ? mainResult : null);
result = DynamicProcedureResultBinder.BindPayload(declaredResultType, resultName, mainResult, res.Where(x => x.Key != resultName).ToDictionary(x => x.Key, x => x.Value), mainResult != null && declaredResultType.IsInstanceOfType(mainResult) ? mainResult : null);
else
result = res.ToDynamic();
}
else if (declaredResultType != null && mainResult != null && declaredResultType.IsInstanceOfType(mainResult))
result = mainResult;
else if (declaredResultType != null)
result = DynamicProcedureResultBinder.BindPayload(declaredResultType, binder.Name, mainResult, null);
result = DynamicProcedureResultBinder.BindPayload(declaredResultType, resultName, mainResult, null);
else
result = mainResult;
@@ -14440,6 +14462,67 @@ namespace DynamORM
return resultTable;
}
}
internal sealed class DynamicProcedureDescriptor
{
public Type ProcedureType { get; set; }
public Type ArgumentsType { get; set; }
public Type ResultType { get; set; }
public string ProcedureName { get; set; }
public string ResultName { get; set; }
internal static DynamicProcedureDescriptor Resolve(Type procedureType)
{
if (procedureType == null)
throw new ArgumentNullException("procedureType");
Type current = procedureType;
Type argumentsType = null;
Type resultType = null;
while (current != null && current != typeof(object))
{
if (current.IsGenericType)
{
Type genericDefinition = current.GetGenericTypeDefinition();
Type[] genericArguments = current.GetGenericArguments();
if (genericDefinition == typeof(Procedure<>) && genericArguments.Length == 1)
{
argumentsType = genericArguments[0];
break;
}
if (genericDefinition == typeof(Procedure<,>) && genericArguments.Length == 2)
{
argumentsType = genericArguments[0];
resultType = genericArguments[1];
break;
}
}
current = current.BaseType;
}
if (argumentsType == null)
throw new InvalidOperationException(string.Format("Type '{0}' is not a typed procedure descriptor.", procedureType.FullName));
if (!typeof(IProcedureParameters).IsAssignableFrom(argumentsType))
throw new InvalidOperationException(string.Format("Procedure descriptor '{0}' declares argument type '{1}' that does not implement IProcedureParameters.", procedureType.FullName, argumentsType.FullName));
ProcedureAttribute attr = procedureType.GetCustomAttributes(typeof(ProcedureAttribute), true)
.Cast<ProcedureAttribute>()
.FirstOrDefault();
string name = attr != null && !string.IsNullOrEmpty(attr.Name) ? attr.Name : procedureType.Name;
string owner = attr != null ? attr.Owner : null;
return new DynamicProcedureDescriptor
{
ProcedureType = procedureType,
ArgumentsType = argumentsType,
ResultType = resultType,
ProcedureName = string.IsNullOrEmpty(owner) ? name : string.Format("{0}.{1}", owner, name),
ResultName = name
};
}
}
internal static class DynamicProcedureParameterBinder
{
internal sealed class BindingResult
@@ -17935,6 +18018,19 @@ namespace DynamORM
public class IgnoreAttribute : Attribute
{
}
/// <summary>Allows to add stored procedure metadata to class.</summary>
[AttributeUsage(AttributeTargets.Class)]
public class ProcedureAttribute : Attribute
{
/// <summary>Gets or sets procedure owner name.</summary>
public string Owner { get; set; }
/// <summary>Gets or sets procedure name.</summary>
public string Name { get; set; }
/// <summary>Gets or sets a value indicating whether metadata overrides other defaults.</summary>
public bool Override { get; set; }
}
/// <summary>Declares metadata for object-based stored procedure parameters.</summary>
[AttributeUsage(AttributeTargets.Property)]
public class ProcedureParameterAttribute : ColumnAttribute
@@ -18456,6 +18552,19 @@ namespace DynamORM
_database = null;
}
}
/// <summary>Base class for typed stored procedure descriptors.</summary>
/// <typeparam name="TArgs">Procedure arguments contract.</typeparam>
public abstract class Procedure<TArgs>
where TArgs : IProcedureParameters
{
}
/// <summary>Base class for typed stored procedure descriptors with explicit result model.</summary>
/// <typeparam name="TArgs">Procedure arguments contract.</typeparam>
/// <typeparam name="TResult">Procedure result model.</typeparam>
public abstract class Procedure<TArgs, TResult> : Procedure<TArgs>
where TArgs : IProcedureParameters
{
}
/// <summary>Marks an object as an explicit stored procedure parameter contract.</summary>
public interface IProcedureParameters
{