diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs
index 696a5ec..4b7b544 100644
--- a/AmalgamationTool/DynamORM.Amalgamation.cs
+++ b/AmalgamationTool/DynamORM.Amalgamation.cs
@@ -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
}
- /// Marks an object as an explicit stored procedure parameter contract.
- public interface IProcedureParameters
- {
- }
- /// Marks an object as a stored procedure parameter contract with a declared typed result model.
- /// Typed result model.
- public interface IProcedureParameters : IProcedureParameters
- {
- }
- /// Allows typed procedure result models to consume multiple result sets directly.
- public interface IProcedureResultReader
- {
- /// Reads all required result sets from the procedure reader.
- /// Procedure result reader, usually a cached reader.
- void ReadResults(IDataReader reader);
- }
- /// Declares metadata for object-based stored procedure parameters.
- [AttributeUsage(AttributeTargets.Property)]
- public class ProcedureParameterAttribute : ColumnAttribute
- {
- /// Sentinel used when database type was not provided.
- public const int UnspecifiedDbType = -1;
-
- /// Sentinel used when size was not provided.
- public const int UnspecifiedSize = -1;
-
- /// Sentinel used when precision or scale was not provided.
- public const byte UnspecifiedByte = byte.MaxValue;
-
- /// Gets or sets parameter direction. Defaults to input.
- public ParameterDirection Direction { get; set; }
-
- /// Gets or sets explicit parameter order. Lower values are emitted first.
- public int Order { get; set; }
-
- /// Gets or sets parameter database type.
- public DbType DbType { get; set; }
-
- /// Gets or sets parameter size.
- public new int Size { get; set; }
-
- /// Gets or sets parameter precision.
- public new byte Precision { get; set; }
-
- /// Gets or sets parameter scale.
- public new byte Scale { get; set; }
-
- /// Initializes a new instance of the class.
- public ProcedureParameterAttribute()
- {
- Direction = ParameterDirection.Input;
- Order = int.MaxValue;
- DbType = (DbType)UnspecifiedDbType;
- Size = UnspecifiedSize;
- Precision = UnspecifiedByte;
- Scale = UnspecifiedByte;
- }
- /// Initializes a new instance of the class.
- public ProcedureParameterAttribute(string name)
- : this()
- {
- Name = name;
- }
- }
namespace Builders
{
/// Typed join kind used by typed fluent builder APIs.
@@ -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 ? "" : resultType.FullName));
+ if (!HasDeclaredResultBinding(resultType))
+ throw new InvalidOperationException(string.Format("Type '{0}' does not declare a supported procedure result binding.", resultType == null ? "" : resultType.FullName));
object instance = CreateDeclaredResult(resultType);
- ((IProcedureResultReader)instance).ReadResults(reader);
+
+ IList 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 returnValues, object existing = null)
@@ -14688,6 +14637,210 @@ namespace DynamORM
return payload.ToDynamic();
}
+ private static IList 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().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 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 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;
+ }
}
/// Framework detection and specific implementations.
public static class FrameworkTools
@@ -17684,6 +17837,72 @@ namespace DynamORM
public class IgnoreAttribute : Attribute
{
}
+ /// Declares metadata for object-based stored procedure parameters.
+ [AttributeUsage(AttributeTargets.Property)]
+ public class ProcedureParameterAttribute : ColumnAttribute
+ {
+ /// Sentinel used when database type was not provided.
+ public const int UnspecifiedDbType = -1;
+
+ /// Sentinel used when size was not provided.
+ public const int UnspecifiedSize = -1;
+
+ /// Sentinel used when precision or scale was not provided.
+ public const byte UnspecifiedByte = byte.MaxValue;
+
+ /// Gets or sets parameter direction. Defaults to input.
+ public ParameterDirection Direction { get; set; }
+
+ /// Gets or sets explicit parameter order. Lower values are emitted first.
+ public int Order { get; set; }
+
+ /// Gets or sets parameter database type.
+ public DbType DbType { get; set; }
+
+ /// Gets or sets parameter size.
+ public new int Size { get; set; }
+
+ /// Gets or sets parameter precision.
+ public new byte Precision { get; set; }
+
+ /// Gets or sets parameter scale.
+ public new byte Scale { get; set; }
+
+ /// Initializes a new instance of the class.
+ public ProcedureParameterAttribute()
+ {
+ Direction = ParameterDirection.Input;
+ Order = int.MaxValue;
+ DbType = (DbType)UnspecifiedDbType;
+ Size = UnspecifiedSize;
+ Precision = UnspecifiedByte;
+ Scale = UnspecifiedByte;
+ }
+ /// Initializes a new instance of the class.
+ public ProcedureParameterAttribute(string name)
+ : this()
+ {
+ Name = name;
+ }
+ }
+ /// Declares mapping of a typed procedure result property to a specific result set.
+ [AttributeUsage(AttributeTargets.Property)]
+ public class ProcedureResultAttribute : Attribute
+ {
+ /// Initializes a new instance of the class.
+ public ProcedureResultAttribute(int resultIndex)
+ {
+ ResultIndex = resultIndex;
+ }
+ /// Gets result-set index in reader order, zero based.
+ public int ResultIndex { get; private set; }
+
+ /// Gets or sets optional column name for scalar/simple list extraction.
+ public string ColumnName { get; set; }
+
+ /// Gets or sets optional name used for DataTable.
+ public string Name { get; set; }
+ }
/// Allows to add table name to class.
[AttributeUsage(AttributeTargets.Class)]
public class TableAttribute : Attribute
@@ -18131,6 +18350,22 @@ namespace DynamORM
_database = null;
}
}
+ /// Marks an object as an explicit stored procedure parameter contract.
+ public interface IProcedureParameters
+ {
+ }
+ /// Marks an object as a stored procedure parameter contract with a declared typed result model.
+ /// Typed result model.
+ public interface IProcedureParameters : IProcedureParameters
+ {
+ }
+ /// Allows typed procedure result models to consume multiple result sets directly.
+ public interface IProcedureResultReader
+ {
+ /// Reads all required result sets from the procedure reader.
+ /// Procedure result reader, usually a cached reader.
+ void ReadResults(IDataReader reader);
+ }
}
namespace TypedSql
{
diff --git a/DynamORM.Tests/Helpers/ProcedureParameterModels.cs b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs
index 2aa2328..70c3077 100644
--- a/DynamORM.Tests/Helpers/ProcedureParameterModels.cs
+++ b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs
@@ -68,6 +68,39 @@ namespace DynamORM.Tests.Helpers
}
}
+ public class ProcedureAttributedResult
+ {
+ [DynamORM.Mapper.Column("sp_Test")]
+ public int MainResult { get; set; }
+
+ [DynamORM.Mapper.Column("status")]
+ public int Status { get; set; }
+
+ [ProcedureResult(0, ColumnName = "Code")]
+ public string FirstCode { get; set; }
+
+ [ProcedureResult(1, ColumnName = "Code")]
+ public System.Collections.Generic.List Codes { get; set; }
+
+ [ProcedureResult(2, ColumnName = "State")]
+ public int[] States { get; set; }
+
+ [ProcedureResult(3)]
+ public Users User { get; set; }
+
+ [ProcedureResult(4)]
+ public System.Collections.Generic.List AllUsers { get; set; }
+
+ [ProcedureResult(5, Name = "codes_table")]
+ public DataTable CodesTable { get; set; }
+ }
+
+ public class ProcedureAttributedResultArgs : IProcedureParameters
+ {
+ [ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)]
+ public int Status { get; set; }
+ }
+
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 e9392bc..70a1c22 100644
--- a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs
+++ b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs
@@ -172,6 +172,40 @@ namespace DynamORM.Tests.Procedure
}
}
+ [Test]
+ public void TestDeclaredResultCanReadAttributedResultSets()
+ {
+ using (var reader = new FakeMultiResultDataReader(
+ Tuple.Create(new[] { "Code" }, new[] { typeof(string) }, new[] { new object[] { "FIRST" }, new object[] { "SECOND" } }),
+ Tuple.Create(new[] { "Code" }, new[] { typeof(string) }, new[] { new object[] { "A" }, new object[] { "B" } }),
+ Tuple.Create(new[] { "State" }, new[] { typeof(int) }, new[] { new object[] { 10 }, new object[] { 20 } }),
+ Tuple.Create(
+ new[] { "id", "code", "first" },
+ new[] { typeof(long), typeof(string), typeof(string) },
+ new[] { new object[] { 1L, "U1", "One" } }),
+ Tuple.Create(
+ new[] { "id", "code", "first" },
+ new[] { typeof(long), typeof(string), typeof(string) },
+ new[] { new object[] { 2L, "U2", "Two" }, new object[] { 3L, "U3", "Three" } }),
+ Tuple.Create(new[] { "Code" }, new[] { typeof(string) }, new[] { new object[] { "X" }, new object[] { "Y" } })))
+ {
+ var result = DynamicProcedureResultBinder.ReadDeclaredResult(typeof(ProcedureAttributedResult), reader) as ProcedureAttributedResult;
+
+ Assert.NotNull(result);
+ Assert.AreEqual("FIRST", result.FirstCode);
+ CollectionAssert.AreEqual(new[] { "A", "B" }, result.Codes);
+ CollectionAssert.AreEqual(new[] { 10, 20 }, result.States);
+ Assert.NotNull(result.User);
+ Assert.AreEqual(1L, result.User.Id);
+ Assert.AreEqual("U1", result.User.Code);
+ Assert.NotNull(result.AllUsers);
+ Assert.AreEqual(2, result.AllUsers.Count);
+ Assert.NotNull(result.CodesTable);
+ Assert.AreEqual("codes_table", result.CodesTable.TableName);
+ Assert.AreEqual(2, result.CodesTable.Rows.Count);
+ }
+ }
+
[Test]
public void TestDeclaredResultPayloadCanAugmentReaderResult()
{
@@ -195,5 +229,13 @@ namespace DynamORM.Tests.Procedure
CollectionAssert.AreEqual(new[] { "A" }, result.Codes);
CollectionAssert.AreEqual(new[] { 10 }, result.States);
}
+
+ [Test]
+ public void TestDeclaredResultBindingDetectionSupportsAttributedResults()
+ {
+ Assert.IsTrue(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureMultiResult)));
+ Assert.IsTrue(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureAttributedResult)));
+ Assert.IsFalse(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureParameterResult)));
+ }
}
}
diff --git a/DynamORM/DynamicProcedureInvoker.cs b/DynamORM/DynamicProcedureInvoker.cs
index 329ca72..c56ed29 100644
--- a/DynamORM/DynamicProcedureInvoker.cs
+++ b/DynamORM/DynamicProcedureInvoker.cs
@@ -282,7 +282,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())
diff --git a/DynamORM/Helpers/DynamicProcedureResultBinder.cs b/DynamORM/Helpers/DynamicProcedureResultBinder.cs
index fa283e9..eb00a8b 100644
--- a/DynamORM/Helpers/DynamicProcedureResultBinder.cs
+++ b/DynamORM/Helpers/DynamicProcedureResultBinder.cs
@@ -30,6 +30,7 @@ using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
+using System.Reflection;
using DynamORM.Mapper;
using DynamORM.Objects;
@@ -37,6 +38,12 @@ namespace DynamORM.Helpers
{
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;
@@ -53,9 +60,10 @@ namespace DynamORM.Helpers
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)
@@ -72,11 +80,17 @@ namespace DynamORM.Helpers
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
{
- if (!CanReadResults(resultType))
- throw new InvalidOperationException(string.Format("Type '{0}' does not implement IProcedureResultReader.", resultType == null ? "" : resultType.FullName));
+ if (!HasDeclaredResultBinding(resultType))
+ throw new InvalidOperationException(string.Format("Type '{0}' does not declare a supported procedure result binding.", resultType == null ? "" : resultType.FullName));
object instance = CreateDeclaredResult(resultType);
- ((IProcedureResultReader)instance).ReadResults(reader);
+
+ IList bindings = GetResultPropertyBindings(resultType);
+ if (bindings.Count > 0)
+ BindResultProperties(instance, reader, bindings);
+ else
+ ((IProcedureResultReader)instance).ReadResults(reader);
+
return instance;
}
@@ -103,5 +117,232 @@ namespace DynamORM.Helpers
return payload.ToDynamic();
}
+
+ private static IList 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().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 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 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;
+ }
}
}
diff --git a/DynamORM/Mapper/ProcedureResultAttribute.cs b/DynamORM/Mapper/ProcedureResultAttribute.cs
new file mode 100644
index 0000000..a99ec3f
--- /dev/null
+++ b/DynamORM/Mapper/ProcedureResultAttribute.cs
@@ -0,0 +1,52 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+using System;
+
+namespace DynamORM.Mapper
+{
+ /// Declares mapping of a typed procedure result property to a specific result set.
+ [AttributeUsage(AttributeTargets.Property)]
+ public class ProcedureResultAttribute : Attribute
+ {
+ /// Initializes a new instance of the class.
+ public ProcedureResultAttribute(int resultIndex)
+ {
+ ResultIndex = resultIndex;
+ }
+
+ /// Gets result-set index in reader order, zero based.
+ public int ResultIndex { get; private set; }
+
+ /// Gets or sets optional column name for scalar/simple list extraction.
+ public string ColumnName { get; set; }
+
+ /// Gets or sets optional name used for DataTable.
+ public string Name { get; set; }
+ }
+}