From 6ebda34a04a40bccd5392b67b7b68e6f82b6fe10 Mon Sep 17 00:00:00 2001 From: Grzegorz Russek Date: Fri, 27 Feb 2026 15:57:06 +0100 Subject: [PATCH] Improve declarative procedure result-set binding --- AmalgamationTool/DynamORM.Amalgamation.cs | 102 ++++++++++++------ .../Helpers/ProcedureParameterModels.cs | 18 ++++ .../ProcedureParameterBinderTests.cs | 37 ++++++- DynamORM/DynamicProcedureInvoker.cs | 2 +- .../Helpers/DynamicProcedureResultBinder.cs | 100 +++++++++++------ DynamORM/Mapper/ProcedureResultAttribute.cs | 2 +- 6 files changed, 194 insertions(+), 67 deletions(-) diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index 4b7b544..7784c0c 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -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 ? "" : resultType.FullName)); object instance = CreateDeclaredResult(resultType); - IList bindings = GetResultPropertyBindings(resultType); + IList 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 GetResultPropertyBindings(Type resultType) + private static IList 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().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().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 bindings) + private static void BindResultMembers(object instance, IDataReader reader, IList 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 bindings) + private static void ValidateBindings(Type resultType, IList 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); } } /// Framework detection and specific implementations. @@ -17886,7 +17924,7 @@ namespace DynamORM } } /// Declares mapping of a typed procedure result property to a specific result set. - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class ProcedureResultAttribute : Attribute { /// Initializes a new instance of the class. diff --git a/DynamORM.Tests/Helpers/ProcedureParameterModels.cs b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs index 70c3077..e86e8e6 100644 --- a/DynamORM.Tests/Helpers/ProcedureParameterModels.cs +++ b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs @@ -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 Codes; + + [ProcedureResult(2, ColumnName = "State")] + public System.Collections.Generic.IEnumerable States; + + [ProcedureResult(3)] + public Users User; + + [ProcedureResult(4, Name = "users_table")] + public DataTable UsersTable; + } + public class ProcedureMultiResultArgs : IProcedureParameters { [ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)] diff --git a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs index 70a1c22..648abe5 100644 --- a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs +++ b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs @@ -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))); } } } diff --git a/DynamORM/DynamicProcedureInvoker.cs b/DynamORM/DynamicProcedureInvoker.cs index c56ed29..329ca72 100644 --- a/DynamORM/DynamicProcedureInvoker.cs +++ b/DynamORM/DynamicProcedureInvoker.cs @@ -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()) diff --git a/DynamORM/Helpers/DynamicProcedureResultBinder.cs b/DynamORM/Helpers/DynamicProcedureResultBinder.cs index eb00a8b..4759935 100644 --- a/DynamORM/Helpers/DynamicProcedureResultBinder.cs +++ b/DynamORM/Helpers/DynamicProcedureResultBinder.cs @@ -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 ? "" : resultType.FullName)); object instance = CreateDeclaredResult(resultType); - IList bindings = GetResultPropertyBindings(resultType); + IList 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 GetResultPropertyBindings(Type resultType) + private static IList 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().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().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 bindings) + private static void BindResultMembers(object instance, IDataReader reader, IList 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 bindings) + private static void ValidateBindings(Type resultType, IList 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); } } } diff --git a/DynamORM/Mapper/ProcedureResultAttribute.cs b/DynamORM/Mapper/ProcedureResultAttribute.cs index a99ec3f..a060834 100644 --- a/DynamORM/Mapper/ProcedureResultAttribute.cs +++ b/DynamORM/Mapper/ProcedureResultAttribute.cs @@ -31,7 +31,7 @@ using System; namespace DynamORM.Mapper { /// Declares mapping of a typed procedure result property to a specific result set. - [AttributeUsage(AttributeTargets.Property)] + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class ProcedureResultAttribute : Attribute { /// Initializes a new instance of the class.