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; } + } +}