Add declarative procedure result-set binding

This commit is contained in:
2026-02-27 15:52:42 +01:00
parent cb6437ee9d
commit 99ff6b3d29
6 changed files with 679 additions and 76 deletions

View File

@@ -10,6 +10,7 @@ using DynamORM.Builders;
using DynamORM.Helpers.Dynamics;
using DynamORM.Helpers;
using DynamORM.Mapper;
using DynamORM.Objects;
using DynamORM.TypedSql;
using DynamORM.Validation;
using System.Collections.Concurrent;
@@ -5541,7 +5542,7 @@ namespace DynamORM
object mainResult = null;
if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType))
if (types.Count == 0 && DynamicProcedureResultBinder.HasDeclaredResultBinding(declaredResultType))
{
using (IDataReader rdr = cmd.ExecuteReader())
using (IDataReader cache = rdr.CachedReader())
@@ -6849,70 +6850,6 @@ namespace DynamORM
#endregion IExtendedDisposable Members
}
/// <summary>Marks an object as an explicit stored procedure parameter contract.</summary>
public interface IProcedureParameters
{
}
/// <summary>Marks an object as a stored procedure parameter contract with a declared typed result model.</summary>
/// <typeparam name="TResult">Typed result model.</typeparam>
public interface IProcedureParameters<TResult> : IProcedureParameters
{
}
/// <summary>Allows typed procedure result models to consume multiple result sets directly.</summary>
public interface IProcedureResultReader
{
/// <summary>Reads all required result sets from the procedure reader.</summary>
/// <param name="reader">Procedure result reader, usually a cached reader.</param>
void ReadResults(IDataReader reader);
}
/// <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>
@@ -14627,6 +14564,11 @@ namespace DynamORM
}
internal static class DynamicProcedureResultBinder
{
private sealed class ResultPropertyBinding
{
public ProcedureResultAttribute Attribute { get; set; }
public PropertyInfo Property { get; set; }
}
internal static bool IsProcedureContract(object item)
{
return item is IProcedureParameters;
@@ -14641,9 +14583,10 @@ namespace DynamORM
return iface == null ? null : iface.GetGenericArguments()[0];
}
internal static bool CanReadResults(Type resultType)
internal static bool HasDeclaredResultBinding(Type resultType)
{
return resultType != null && typeof(IProcedureResultReader).IsAssignableFrom(resultType);
return resultType != null &&
(typeof(IProcedureResultReader).IsAssignableFrom(resultType) || GetResultPropertyBindings(resultType).Count > 0);
}
internal static object CreateDeclaredResult(Type resultType)
{
@@ -14658,11 +14601,17 @@ namespace DynamORM
}
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
{
if (!CanReadResults(resultType))
throw new InvalidOperationException(string.Format("Type '{0}' does not implement IProcedureResultReader.", resultType == null ? "<null>" : resultType.FullName));
if (!HasDeclaredResultBinding(resultType))
throw new InvalidOperationException(string.Format("Type '{0}' does not declare a supported procedure result binding.", resultType == null ? "<null>" : resultType.FullName));
object instance = CreateDeclaredResult(resultType);
((IProcedureResultReader)instance).ReadResults(reader);
IList<ResultPropertyBinding> bindings = GetResultPropertyBindings(resultType);
if (bindings.Count > 0)
BindResultProperties(instance, reader, bindings);
else
((IProcedureResultReader)instance).ReadResults(reader);
return instance;
}
internal static object BindPayload(Type resultType, string mainResultName, object mainResult, IDictionary<string, object> returnValues, object existing = null)
@@ -14688,6 +14637,210 @@ namespace DynamORM
return payload.ToDynamic();
}
private static IList<ResultPropertyBinding> GetResultPropertyBindings(Type resultType)
{
return resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanWrite && x.GetIndexParameters().Length == 0)
.Select(x => new ResultPropertyBinding
{
Property = x,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
})
.Where(x => x.Attribute != null)
.OrderBy(x => x.Attribute.ResultIndex)
.ThenBy(x => x.Property.MetadataToken)
.ToList();
}
private static void BindResultProperties(object instance, IDataReader reader, IList<ResultPropertyBinding> bindings)
{
ValidateBindings(instance.GetType(), bindings);
int currentIndex = 0;
bool hasCurrent = true;
for (int i = 0; i < bindings.Count; i++)
{
ResultPropertyBinding binding = bindings[i];
while (hasCurrent && currentIndex < binding.Attribute.ResultIndex)
{
hasCurrent = reader.NextResult();
currentIndex++;
}
if (!hasCurrent || currentIndex != binding.Attribute.ResultIndex)
break;
object value = ReadResultValue(binding.Property.PropertyType, binding.Attribute, reader);
binding.Property.SetValue(instance, value, null);
if (i + 1 < bindings.Count)
{
hasCurrent = reader.NextResult();
currentIndex++;
}
}
}
private static void ValidateBindings(Type resultType, IList<ResultPropertyBinding> bindings)
{
var duplicates = bindings.GroupBy(x => x.Attribute.ResultIndex).FirstOrDefault(x => x.Count() > 1);
if (duplicates != null)
throw new InvalidOperationException(string.Format("Type '{0}' defines multiple ProcedureResultAttribute bindings for result index {1}.", resultType.FullName, duplicates.Key));
}
private static object ReadResultValue(Type propertyType, ProcedureResultAttribute attr, IDataReader reader)
{
Type elementType;
if (propertyType == typeof(DataTable))
return reader.ToDataTable(string.IsNullOrEmpty(attr.Name) ? null : attr.Name);
if (TryGetEnumerableElementType(propertyType, out elementType))
{
if (elementType == typeof(object))
return reader.EnumerateReader().ToList();
if (elementType.IsValueType || elementType == typeof(string) || elementType == typeof(Guid))
return ReadSimpleList(propertyType, elementType, attr, reader);
return ReadComplexList(propertyType, elementType, reader);
}
if (propertyType.IsValueType || propertyType == typeof(string) || propertyType == typeof(Guid))
return ReadSimpleValue(propertyType, attr, reader);
return ReadComplexValue(propertyType, reader);
}
private static object ReadSimpleValue(Type propertyType, ProcedureResultAttribute attr, IDataReader reader)
{
object value = null;
int ordinal = -1;
bool haveRow = false;
while (reader.Read())
{
if (!haveRow)
{
ordinal = GetOrdinal(reader, attr);
value = reader.IsDBNull(ordinal) ? null : reader[ordinal];
haveRow = true;
}
}
if (!haveRow || value == null)
return propertyType.GetDefaultValue();
if (propertyType == typeof(Guid))
{
Guid g;
if (Guid.TryParse(value.ToString(), out g))
return g;
return propertyType.GetDefaultValue();
}
return propertyType.CastObject(value);
}
private static object ReadSimpleList(Type propertyType, Type elementType, ProcedureResultAttribute attr, IDataReader reader)
{
Type listType = typeof(List<>).MakeGenericType(elementType);
var list = (System.Collections.IList)Activator.CreateInstance(listType);
int ordinal = -1;
bool initialized = false;
while (reader.Read())
{
if (!initialized)
{
ordinal = GetOrdinal(reader, attr);
initialized = true;
}
if (reader.IsDBNull(ordinal))
{
list.Add(elementType.GetDefaultValue());
continue;
}
object value = reader[ordinal];
if (elementType == typeof(Guid))
{
Guid g;
list.Add(Guid.TryParse(value.ToString(), out g) ? (object)g : elementType.GetDefaultValue());
}
else
list.Add(elementType.CastObject(value));
}
if (propertyType.IsArray)
{
Array array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
return list;
}
private static object ReadComplexValue(Type propertyType, IDataReader reader)
{
object value = null;
bool haveRow = false;
while (reader.Read())
{
if (!haveRow)
{
value = (reader.RowToDynamic() as object).Map(propertyType);
haveRow = true;
}
}
return value;
}
private static object ReadComplexList(Type propertyType, Type elementType, IDataReader reader)
{
Type listType = typeof(List<>).MakeGenericType(elementType);
var list = (System.Collections.IList)Activator.CreateInstance(listType);
while (reader.Read())
list.Add((reader.RowToDynamic() as object).Map(elementType));
if (propertyType.IsArray)
{
Array array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
return list;
}
private static bool TryGetEnumerableElementType(Type type, out Type elementType)
{
elementType = null;
if (type == typeof(string) || type == typeof(byte[]))
return false;
if (type.IsArray)
{
elementType = type.GetElementType();
return true;
}
if (type.IsGenericType)
{
Type generic = type.GetGenericTypeDefinition();
if (generic == typeof(List<>) || generic == typeof(IList<>) || generic == typeof(IEnumerable<>))
{
elementType = type.GetGenericArguments()[0];
return true;
}
}
Type enumerableInterface = type.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
if (enumerableInterface != null)
{
elementType = enumerableInterface.GetGenericArguments()[0];
return true;
}
return false;
}
private static int GetOrdinal(IDataReader reader, ProcedureResultAttribute attr)
{
if (attr == null || string.IsNullOrEmpty(attr.ColumnName))
return 0;
int ordinal = reader.GetOrdinal(attr.ColumnName);
if (ordinal < 0)
throw new IndexOutOfRangeException(attr.ColumnName);
return ordinal;
}
}
/// <summary>Framework detection and specific implementations.</summary>
public static class FrameworkTools
@@ -17684,6 +17837,72 @@ namespace DynamORM
public class IgnoreAttribute : Attribute
{
}
/// <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;
}
}
/// <summary>Declares mapping of a typed procedure result property to a specific result set.</summary>
[AttributeUsage(AttributeTargets.Property)]
public class ProcedureResultAttribute : Attribute
{
/// <summary>Initializes a new instance of the <see cref="ProcedureResultAttribute"/> class.</summary>
public ProcedureResultAttribute(int resultIndex)
{
ResultIndex = resultIndex;
}
/// <summary>Gets result-set index in reader order, zero based.</summary>
public int ResultIndex { get; private set; }
/// <summary>Gets or sets optional column name for scalar/simple list extraction.</summary>
public string ColumnName { get; set; }
/// <summary>Gets or sets optional name used for DataTable.</summary>
public string Name { get; set; }
}
/// <summary>Allows to add table name to class.</summary>
[AttributeUsage(AttributeTargets.Class)]
public class TableAttribute : Attribute
@@ -18131,6 +18350,22 @@ namespace DynamORM
_database = null;
}
}
/// <summary>Marks an object as an explicit stored procedure parameter contract.</summary>
public interface IProcedureParameters
{
}
/// <summary>Marks an object as a stored procedure parameter contract with a declared typed result model.</summary>
/// <typeparam name="TResult">Typed result model.</typeparam>
public interface IProcedureParameters<TResult> : IProcedureParameters
{
}
/// <summary>Allows typed procedure result models to consume multiple result sets directly.</summary>
public interface IProcedureResultReader
{
/// <summary>Reads all required result sets from the procedure reader.</summary>
/// <param name="reader">Procedure result reader, usually a cached reader.</param>
void ReadResults(IDataReader reader);
}
}
namespace TypedSql
{