Improve declarative procedure result-set binding

This commit is contained in:
2026-02-27 15:57:06 +01:00
parent 99ff6b3d29
commit 6ebda34a04
6 changed files with 194 additions and 67 deletions

View File

@@ -5542,7 +5542,7 @@ namespace DynamORM
object mainResult = null;
if (types.Count == 0 && DynamicProcedureResultBinder.HasDeclaredResultBinding(declaredResultType))
if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType))
{
using (IDataReader rdr = cmd.ExecuteReader())
using (IDataReader cache = rdr.CachedReader())
@@ -14564,10 +14564,25 @@ namespace DynamORM
}
internal static class DynamicProcedureResultBinder
{
private sealed class ResultPropertyBinding
private sealed class ResultMemberBinding
{
public ProcedureResultAttribute Attribute { get; set; }
public PropertyInfo Property { get; set; }
public MemberInfo Member { get; set; }
public Type MemberType { get; set; }
public int SortOrder { get; set; }
public void SetValue(object instance, object value)
{
PropertyInfo property = Member as PropertyInfo;
if (property != null)
{
property.SetValue(instance, value, null);
return;
}
FieldInfo field = Member as FieldInfo;
if (field != null)
field.SetValue(instance, value);
}
}
internal static bool IsProcedureContract(object item)
{
@@ -14583,10 +14598,14 @@ namespace DynamORM
return iface == null ? null : iface.GetGenericArguments()[0];
}
internal static bool HasDeclaredResultBinding(Type resultType)
internal static bool CanReadResults(Type resultType)
{
return resultType != null &&
(typeof(IProcedureResultReader).IsAssignableFrom(resultType) || GetResultPropertyBindings(resultType).Count > 0);
(typeof(IProcedureResultReader).IsAssignableFrom(resultType) || GetResultMemberBindings(resultType).Count > 0);
}
internal static bool HasDeclaredResultBinding(Type resultType)
{
return CanReadResults(resultType);
}
internal static object CreateDeclaredResult(Type resultType)
{
@@ -14601,14 +14620,14 @@ namespace DynamORM
}
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
{
if (!HasDeclaredResultBinding(resultType))
if (!CanReadResults(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);
IList<ResultPropertyBinding> bindings = GetResultPropertyBindings(resultType);
IList<ResultMemberBinding> bindings = GetResultMemberBindings(resultType);
if (bindings.Count > 0)
BindResultProperties(instance, reader, bindings);
BindResultMembers(instance, reader, bindings);
else
((IProcedureResultReader)instance).ReadResults(reader);
@@ -14637,21 +14656,35 @@ namespace DynamORM
return payload.ToDynamic();
}
private static IList<ResultPropertyBinding> GetResultPropertyBindings(Type resultType)
private static IList<ResultMemberBinding> GetResultMemberBindings(Type resultType)
{
return resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
var properties = resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanWrite && x.GetIndexParameters().Length == 0)
.Select(x => new ResultPropertyBinding
.Select(x => new ResultMemberBinding
{
Property = x,
Member = x,
MemberType = x.PropertyType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
})
});
var fields = resultType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Where(x => !x.IsInitOnly && !x.IsLiteral)
.Select(x => new ResultMemberBinding
{
Member = x,
MemberType = x.FieldType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
});
return properties.Concat(fields)
.Where(x => x.Attribute != null)
.OrderBy(x => x.Attribute.ResultIndex)
.ThenBy(x => x.Property.MetadataToken)
.ThenBy(x => x.SortOrder)
.ToList();
}
private static void BindResultProperties(object instance, IDataReader reader, IList<ResultPropertyBinding> bindings)
private static void BindResultMembers(object instance, IDataReader reader, IList<ResultMemberBinding> bindings)
{
ValidateBindings(instance.GetType(), bindings);
@@ -14660,7 +14693,7 @@ namespace DynamORM
for (int i = 0; i < bindings.Count; i++)
{
ResultPropertyBinding binding = bindings[i];
ResultMemberBinding binding = bindings[i];
while (hasCurrent && currentIndex < binding.Attribute.ResultIndex)
{
hasCurrent = reader.NextResult();
@@ -14669,8 +14702,8 @@ namespace DynamORM
if (!hasCurrent || currentIndex != binding.Attribute.ResultIndex)
break;
object value = ReadResultValue(binding.Property.PropertyType, binding.Attribute, reader);
binding.Property.SetValue(instance, value, null);
object value = ReadResultValue(binding.MemberType, binding.Attribute, reader);
binding.SetValue(instance, value);
if (i + 1 < bindings.Count)
{
@@ -14679,7 +14712,7 @@ namespace DynamORM
}
}
}
private static void ValidateBindings(Type resultType, IList<ResultPropertyBinding> bindings)
private static void ValidateBindings(Type resultType, IList<ResultMemberBinding> bindings)
{
var duplicates = bindings.GroupBy(x => x.Attribute.ResultIndex).FirstOrDefault(x => x.Count() > 1);
if (duplicates != null)
@@ -14709,6 +14742,7 @@ namespace DynamORM
}
private static object ReadSimpleValue(Type propertyType, ProcedureResultAttribute attr, IDataReader reader)
{
Type targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
object value = null;
int ordinal = -1;
bool haveRow = false;
@@ -14725,17 +14759,21 @@ namespace DynamORM
if (!haveRow || value == null)
return propertyType.GetDefaultValue();
if (propertyType == typeof(Guid))
if (targetType == typeof(Guid))
{
Guid g;
if (Guid.TryParse(value.ToString(), out g))
return g;
return propertyType == typeof(Guid) ? (object)g : new Guid?(g);
return propertyType.GetDefaultValue();
}
return propertyType.CastObject(value);
if (targetType.IsEnum)
return Enum.ToObject(targetType, value);
return targetType == propertyType ? propertyType.CastObject(value) : targetType.CastObject(value);
}
private static object ReadSimpleList(Type propertyType, Type elementType, ProcedureResultAttribute attr, IDataReader reader)
{
Type targetElementType = Nullable.GetUnderlyingType(elementType) ?? elementType;
Type listType = typeof(List<>).MakeGenericType(elementType);
var list = (System.Collections.IList)Activator.CreateInstance(listType);
int ordinal = -1;
@@ -14754,13 +14792,18 @@ namespace DynamORM
continue;
}
object value = reader[ordinal];
if (elementType == typeof(Guid))
if (targetElementType == typeof(Guid))
{
Guid g;
list.Add(Guid.TryParse(value.ToString(), out g) ? (object)g : elementType.GetDefaultValue());
if (Guid.TryParse(value.ToString(), out g))
list.Add(elementType == typeof(Guid) ? (object)g : new Guid?(g));
else
list.Add(elementType.GetDefaultValue());
}
else if (targetElementType.IsEnum)
list.Add(Enum.ToObject(targetElementType, value));
else
list.Add(elementType.CastObject(value));
list.Add(targetElementType == elementType ? elementType.CastObject(value) : targetElementType.CastObject(value));
}
if (propertyType.IsArray)
{
@@ -14834,12 +14877,7 @@ namespace DynamORM
{
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;
return reader.GetOrdinal(attr.ColumnName);
}
}
/// <summary>Framework detection and specific implementations.</summary>
@@ -17886,7 +17924,7 @@ namespace DynamORM
}
}
/// <summary>Declares mapping of a typed procedure result property to a specific result set.</summary>
[AttributeUsage(AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class ProcedureResultAttribute : Attribute
{
/// <summary>Initializes a new instance of the <see cref="ProcedureResultAttribute"/> class.</summary>

View File

@@ -101,6 +101,24 @@ namespace DynamORM.Tests.Helpers
public int Status { get; set; }
}
public class ProcedureAttributedFieldResult
{
[ProcedureResult(0)]
public string FirstCode;
[ProcedureResult(1)]
public System.Collections.Generic.List<string> Codes;
[ProcedureResult(2, ColumnName = "State")]
public System.Collections.Generic.IEnumerable<int> States;
[ProcedureResult(3)]
public Users User;
[ProcedureResult(4, Name = "users_table")]
public DataTable UsersTable;
}
public class ProcedureMultiResultArgs : IProcedureParameters<ProcedureMultiResult>
{
[ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)]

View File

@@ -206,6 +206,36 @@ namespace DynamORM.Tests.Procedure
}
}
[Test]
public void TestDeclaredResultCanReadAttributedFieldResultSets()
{
using (var reader = new FakeMultiResultDataReader(
Tuple.Create(new[] { "Code" }, new[] { typeof(string) }, new[] { new object[] { "FIELD-FIRST" }, new object[] { "FIELD-SECOND" } }),
Tuple.Create(new[] { "Code" }, new[] { typeof(string) }, new[] { new object[] { "C" }, new object[] { "D" } }),
Tuple.Create(new[] { "State" }, new[] { typeof(int) }, new[] { new object[] { 30 }, new object[] { 40 } }),
Tuple.Create(
new[] { "id", "code", "first" },
new[] { typeof(long), typeof(string), typeof(string) },
new[] { new object[] { 4L, "U4", "Four" } }),
Tuple.Create(
new[] { "id", "code", "first" },
new[] { typeof(long), typeof(string), typeof(string) },
new[] { new object[] { 5L, "U5", "Five" }, new object[] { 6L, "U6", "Six" } })))
{
var result = DynamicProcedureResultBinder.ReadDeclaredResult(typeof(ProcedureAttributedFieldResult), reader) as ProcedureAttributedFieldResult;
Assert.NotNull(result);
Assert.AreEqual("FIELD-FIRST", result.FirstCode);
CollectionAssert.AreEqual(new[] { "C", "D" }, result.Codes);
CollectionAssert.AreEqual(new[] { 30, 40 }, result.States);
Assert.NotNull(result.User);
Assert.AreEqual(4L, result.User.Id);
Assert.NotNull(result.UsersTable);
Assert.AreEqual("users_table", result.UsersTable.TableName);
Assert.AreEqual(2, result.UsersTable.Rows.Count);
}
}
[Test]
public void TestDeclaredResultPayloadCanAugmentReaderResult()
{
@@ -233,9 +263,10 @@ namespace DynamORM.Tests.Procedure
[Test]
public void TestDeclaredResultBindingDetectionSupportsAttributedResults()
{
Assert.IsTrue(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureMultiResult)));
Assert.IsTrue(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureAttributedResult)));
Assert.IsFalse(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureParameterResult)));
Assert.IsTrue(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureMultiResult)));
Assert.IsTrue(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureAttributedResult)));
Assert.IsTrue(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureAttributedFieldResult)));
Assert.IsFalse(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureParameterResult)));
}
}
}

View File

@@ -282,7 +282,7 @@ namespace DynamORM
object mainResult = null;
if (types.Count == 0 && DynamicProcedureResultBinder.HasDeclaredResultBinding(declaredResultType))
if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType))
{
using (IDataReader rdr = cmd.ExecuteReader())
using (IDataReader cache = rdr.CachedReader())

View File

@@ -38,10 +38,26 @@ namespace DynamORM.Helpers
{
internal static class DynamicProcedureResultBinder
{
private sealed class ResultPropertyBinding
private sealed class ResultMemberBinding
{
public ProcedureResultAttribute Attribute { get; set; }
public PropertyInfo Property { get; set; }
public MemberInfo Member { get; set; }
public Type MemberType { get; set; }
public int SortOrder { get; set; }
public void SetValue(object instance, object value)
{
PropertyInfo property = Member as PropertyInfo;
if (property != null)
{
property.SetValue(instance, value, null);
return;
}
FieldInfo field = Member as FieldInfo;
if (field != null)
field.SetValue(instance, value);
}
}
internal static bool IsProcedureContract(object item)
@@ -60,10 +76,15 @@ namespace DynamORM.Helpers
return iface == null ? null : iface.GetGenericArguments()[0];
}
internal static bool HasDeclaredResultBinding(Type resultType)
internal static bool CanReadResults(Type resultType)
{
return resultType != null &&
(typeof(IProcedureResultReader).IsAssignableFrom(resultType) || GetResultPropertyBindings(resultType).Count > 0);
(typeof(IProcedureResultReader).IsAssignableFrom(resultType) || GetResultMemberBindings(resultType).Count > 0);
}
internal static bool HasDeclaredResultBinding(Type resultType)
{
return CanReadResults(resultType);
}
internal static object CreateDeclaredResult(Type resultType)
@@ -80,14 +101,14 @@ namespace DynamORM.Helpers
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
{
if (!HasDeclaredResultBinding(resultType))
if (!CanReadResults(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);
IList<ResultPropertyBinding> bindings = GetResultPropertyBindings(resultType);
IList<ResultMemberBinding> bindings = GetResultMemberBindings(resultType);
if (bindings.Count > 0)
BindResultProperties(instance, reader, bindings);
BindResultMembers(instance, reader, bindings);
else
((IProcedureResultReader)instance).ReadResults(reader);
@@ -118,22 +139,36 @@ namespace DynamORM.Helpers
return payload.ToDynamic();
}
private static IList<ResultPropertyBinding> GetResultPropertyBindings(Type resultType)
private static IList<ResultMemberBinding> GetResultMemberBindings(Type resultType)
{
return resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
var properties = resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanWrite && x.GetIndexParameters().Length == 0)
.Select(x => new ResultPropertyBinding
.Select(x => new ResultMemberBinding
{
Property = x,
Member = x,
MemberType = x.PropertyType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
})
});
var fields = resultType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Where(x => !x.IsInitOnly && !x.IsLiteral)
.Select(x => new ResultMemberBinding
{
Member = x,
MemberType = x.FieldType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
});
return properties.Concat(fields)
.Where(x => x.Attribute != null)
.OrderBy(x => x.Attribute.ResultIndex)
.ThenBy(x => x.Property.MetadataToken)
.ThenBy(x => x.SortOrder)
.ToList();
}
private static void BindResultProperties(object instance, IDataReader reader, IList<ResultPropertyBinding> bindings)
private static void BindResultMembers(object instance, IDataReader reader, IList<ResultMemberBinding> bindings)
{
ValidateBindings(instance.GetType(), bindings);
@@ -142,7 +177,7 @@ namespace DynamORM.Helpers
for (int i = 0; i < bindings.Count; i++)
{
ResultPropertyBinding binding = bindings[i];
ResultMemberBinding binding = bindings[i];
while (hasCurrent && currentIndex < binding.Attribute.ResultIndex)
{
hasCurrent = reader.NextResult();
@@ -152,8 +187,8 @@ namespace DynamORM.Helpers
if (!hasCurrent || currentIndex != binding.Attribute.ResultIndex)
break;
object value = ReadResultValue(binding.Property.PropertyType, binding.Attribute, reader);
binding.Property.SetValue(instance, value, null);
object value = ReadResultValue(binding.MemberType, binding.Attribute, reader);
binding.SetValue(instance, value);
if (i + 1 < bindings.Count)
{
@@ -163,7 +198,7 @@ namespace DynamORM.Helpers
}
}
private static void ValidateBindings(Type resultType, IList<ResultPropertyBinding> bindings)
private static void ValidateBindings(Type resultType, IList<ResultMemberBinding> bindings)
{
var duplicates = bindings.GroupBy(x => x.Attribute.ResultIndex).FirstOrDefault(x => x.Count() > 1);
if (duplicates != null)
@@ -196,6 +231,7 @@ namespace DynamORM.Helpers
private static object ReadSimpleValue(Type propertyType, ProcedureResultAttribute attr, IDataReader reader)
{
Type targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
object value = null;
int ordinal = -1;
bool haveRow = false;
@@ -213,19 +249,23 @@ namespace DynamORM.Helpers
if (!haveRow || value == null)
return propertyType.GetDefaultValue();
if (propertyType == typeof(Guid))
if (targetType == typeof(Guid))
{
Guid g;
if (Guid.TryParse(value.ToString(), out g))
return g;
return propertyType == typeof(Guid) ? (object)g : new Guid?(g);
return propertyType.GetDefaultValue();
}
return propertyType.CastObject(value);
if (targetType.IsEnum)
return Enum.ToObject(targetType, value);
return targetType == propertyType ? propertyType.CastObject(value) : targetType.CastObject(value);
}
private static object ReadSimpleList(Type propertyType, Type elementType, ProcedureResultAttribute attr, IDataReader reader)
{
Type targetElementType = Nullable.GetUnderlyingType(elementType) ?? elementType;
Type listType = typeof(List<>).MakeGenericType(elementType);
var list = (System.Collections.IList)Activator.CreateInstance(listType);
int ordinal = -1;
@@ -246,13 +286,18 @@ namespace DynamORM.Helpers
}
object value = reader[ordinal];
if (elementType == typeof(Guid))
if (targetElementType == typeof(Guid))
{
Guid g;
list.Add(Guid.TryParse(value.ToString(), out g) ? (object)g : elementType.GetDefaultValue());
if (Guid.TryParse(value.ToString(), out g))
list.Add(elementType == typeof(Guid) ? (object)g : new Guid?(g));
else
list.Add(elementType.GetDefaultValue());
}
else if (targetElementType.IsEnum)
list.Add(Enum.ToObject(targetElementType, value));
else
list.Add(elementType.CastObject(value));
list.Add(targetElementType == elementType ? elementType.CastObject(value) : targetElementType.CastObject(value));
}
if (propertyType.IsArray)
@@ -337,12 +382,7 @@ namespace DynamORM.Helpers
{
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;
return reader.GetOrdinal(attr.ColumnName);
}
}
}

View File

@@ -31,7 +31,7 @@ using System;
namespace DynamORM.Mapper
{
/// <summary>Declares mapping of a typed procedure result property to a specific result set.</summary>
[AttributeUsage(AttributeTargets.Property)]
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class ProcedureResultAttribute : Attribute
{
/// <summary>Initializes a new instance of the <see cref="ProcedureResultAttribute"/> class.</summary>