diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index ab08dbc..696a5ec 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -5358,7 +5358,8 @@ namespace DynamORM CallInfo info = binder.CallInfo; // Get generic types - IList types = binder.GetGenericTypeArguments(); + IList types = binder.GetGenericTypeArguments() ?? new List(); + Type declaredResultType = null; Dictionary retParams = null; @@ -5374,6 +5375,7 @@ namespace DynamORM int alen = args.Length; bool retIsAdded = false; + declaredResultType = alen == 1 ? DynamicProcedureResultBinder.GetDeclaredResultType(args[0]) : null; if (alen > 0) { @@ -5539,7 +5541,13 @@ namespace DynamORM object mainResult = null; - if (types.Count > 0) + if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType)) + { + using (IDataReader rdr = cmd.ExecuteReader()) + using (IDataReader cache = rdr.CachedReader()) + mainResult = DynamicProcedureResultBinder.ReadDeclaredResult(declaredResultType, cache); + } + else if (types.Count > 0) { mainResult = types[0].GetDefaultValue(); @@ -5695,9 +5703,15 @@ namespace DynamORM else result = res.ToDynamic(); } + else if (declaredResultType != null) + result = DynamicProcedureResultBinder.BindPayload(declaredResultType, binder.Name, mainResult, res.Where(x => x.Key != binder.Name).ToDictionary(x => x.Key, x => x.Value), mainResult != null && declaredResultType.IsInstanceOfType(mainResult) ? mainResult : null); else result = res.ToDynamic(); } + else if (declaredResultType != null && mainResult != null && declaredResultType.IsInstanceOfType(mainResult)) + result = mainResult; + else if (declaredResultType != null) + result = DynamicProcedureResultBinder.BindPayload(declaredResultType, binder.Name, mainResult, null); else result = mainResult; @@ -6835,6 +6849,22 @@ 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 @@ -14482,15 +14512,10 @@ namespace DynamORM } internal static bool CanBind(object item) { - if (item == null) + if (!DynamicProcedureResultBinder.IsProcedureContract(item)) return false; - Type type = item.GetType(); - - if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(Guid) || type == typeof(TimeSpan)) - return false; - - return GetBindableProperties(type).Any(); + return GetBindableProperties(item.GetType()).Any(); } internal static BindingResult Bind(DynamicDatabase db, IDbCommand cmd, object item) { @@ -14600,6 +14625,70 @@ namespace DynamORM return 0; } } + internal static class DynamicProcedureResultBinder + { + internal static bool IsProcedureContract(object item) + { + return item is IProcedureParameters; + } + internal static Type GetDeclaredResultType(object item) + { + if (item == null) + return null; + + Type iface = item.GetType().GetInterfaces() + .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IProcedureParameters<>)); + + return iface == null ? null : iface.GetGenericArguments()[0]; + } + internal static bool CanReadResults(Type resultType) + { + return resultType != null && typeof(IProcedureResultReader).IsAssignableFrom(resultType); + } + internal static object CreateDeclaredResult(Type resultType) + { + if (resultType == null) + return null; + + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(resultType); + if (mapper != null) + return mapper.Creator(); + + return Activator.CreateInstance(resultType); + } + 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)); + + object instance = CreateDeclaredResult(resultType); + ((IProcedureResultReader)instance).ReadResults(reader); + return instance; + } + internal static object BindPayload(Type resultType, string mainResultName, object mainResult, IDictionary returnValues, object existing = null) + { + if (resultType == null) + return existing ?? returnValues.ToDynamic(); + + Dictionary payload = new Dictionary(); + + if (mainResultName != null) + payload[mainResultName] = mainResult == DBNull.Value ? null : mainResult; + + if (returnValues != null) + foreach (KeyValuePair item in returnValues) + payload[item.Key] = item.Value == DBNull.Value ? null : item.Value; + + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(resultType); + if (mapper != null) + return mapper.Map(payload.ToDynamic(), existing ?? mapper.Creator()); + + if (existing != null) + return existing; + + return payload.ToDynamic(); + } + } /// Framework detection and specific implementations. public static class FrameworkTools { diff --git a/DynamORM.Tests/Helpers/FakeMultiResultDataReader.cs b/DynamORM.Tests/Helpers/FakeMultiResultDataReader.cs new file mode 100644 index 0000000..f94b5a8 --- /dev/null +++ b/DynamORM.Tests/Helpers/FakeMultiResultDataReader.cs @@ -0,0 +1,135 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System; +using System.Collections.Generic; +using System.Data; + +namespace DynamORM.Tests.Helpers +{ + internal sealed class FakeMultiResultDataReader : IDataReader + { + private sealed class ResultSet + { + public string[] Names; + public Type[] Types; + public object[][] Rows; + } + + private readonly List _sets = new List(); + private int _setIndex; + private int _rowIndex = -1; + + public FakeMultiResultDataReader(params Tuple[] sets) + { + foreach (var set in sets) + _sets.Add(new ResultSet + { + Names = set.Item1, + Types = set.Item2, + Rows = set.Item3 + }); + } + + private ResultSet Current { get { return _sets[_setIndex]; } } + + public object this[string name] { get { return GetValue(GetOrdinal(name)); } } + public object this[int i] { get { return GetValue(i); } } + public int Depth { get { return 0; } } + public bool IsClosed { get; private set; } + public int RecordsAffected { get { return 0; } } + public int FieldCount { get { return Current.Names.Length; } } + + public void Close() { IsClosed = true; } + public void Dispose() { Close(); } + public bool GetBoolean(int i) { return (bool)GetValue(i); } + public byte GetByte(int i) { return (byte)GetValue(i); } + public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) { throw new NotSupportedException(); } + public char GetChar(int i) { return (char)GetValue(i); } + public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) { throw new NotSupportedException(); } + public IDataReader GetData(int i) { throw new NotSupportedException(); } + public string GetDataTypeName(int i) { return GetFieldType(i).Name; } + public DateTime GetDateTime(int i) { return (DateTime)GetValue(i); } + public decimal GetDecimal(int i) { return (decimal)GetValue(i); } + public double GetDouble(int i) { return (double)GetValue(i); } + public Type GetFieldType(int i) { return Current.Types[i]; } + public float GetFloat(int i) { return (float)GetValue(i); } + public Guid GetGuid(int i) { return (Guid)GetValue(i); } + public short GetInt16(int i) { return Convert.ToInt16(GetValue(i)); } + public int GetInt32(int i) { return Convert.ToInt32(GetValue(i)); } + public long GetInt64(int i) { return Convert.ToInt64(GetValue(i)); } + public string GetName(int i) { return Current.Names[i]; } + public int GetOrdinal(string name) + { + for (int i = 0; i < Current.Names.Length; i++) + if (string.Equals(Current.Names[i], name, StringComparison.OrdinalIgnoreCase)) + return i; + return -1; + } + public DataTable GetSchemaTable() + { + DataTable schema = new DataTable(); + schema.Columns.Add("ColumnName", typeof(string)); + schema.Columns.Add("ColumnOrdinal", typeof(int)); + schema.Columns.Add("ColumnSize", typeof(int)); + schema.Columns.Add("NumericPrecision", typeof(short)); + schema.Columns.Add("NumericScale", typeof(short)); + schema.Columns.Add("DataType", typeof(Type)); + schema.Columns.Add("ProviderType", typeof(int)); + schema.Columns.Add("NativeType", typeof(int)); + schema.Columns.Add("AllowDBNull", typeof(bool)); + schema.Columns.Add("IsUnique", typeof(bool)); + schema.Columns.Add("IsKey", typeof(bool)); + schema.Columns.Add("IsAutoIncrement", typeof(bool)); + + for (int i = 0; i < Current.Names.Length; i++) + { + DataRow row = schema.NewRow(); + row[0] = Current.Names[i]; + row[1] = i; + row[2] = 0; + row[3] = 0; + row[4] = 0; + row[5] = Current.Types[i]; + row[6] = 0; + row[7] = 0; + row[8] = true; + row[9] = false; + row[10] = false; + row[11] = false; + schema.Rows.Add(row); + } + + return schema; + } + public string GetString(int i) { return (string)GetValue(i); } + public object GetValue(int i) { return Current.Rows[_rowIndex][i]; } + public int GetValues(object[] values) + { + int count = Math.Min(values.Length, FieldCount); + for (int i = 0; i < count; i++) + values[i] = GetValue(i); + return count; + } + public bool IsDBNull(int i) { return GetValue(i) == null || GetValue(i) == DBNull.Value; } + public bool NextResult() + { + if (_setIndex + 1 >= _sets.Count) + return false; + + _setIndex++; + _rowIndex = -1; + return true; + } + public bool Read() + { + if (_rowIndex + 1 >= Current.Rows.Length) + return false; + _rowIndex++; + return true; + } + } +} diff --git a/DynamORM.Tests/Helpers/ProcedureParameterModels.cs b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs index 1581645..d753425 100644 --- a/DynamORM.Tests/Helpers/ProcedureParameterModels.cs +++ b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs @@ -8,7 +8,7 @@ using System.Data; namespace DynamORM.Tests.Helpers { - public class ProcedureParameterObject + public class ProcedureParameterObject : IProcedureParameters { [ProcedureParameter("code", Order = 2, DbType = DbType.String, Size = 32)] public string Code { get; set; } @@ -23,9 +23,52 @@ namespace DynamORM.Tests.Helpers public int Status { get; set; } } - public class ProcedureParameterColumnFallbackObject + public class ProcedureParameterColumnFallbackObject : IProcedureParameters { [DynamORM.Mapper.Column("code", false, DbType.String, 64)] public string Code { get; set; } } + + public class ProcedureParameterResult + { + [DynamORM.Mapper.Column("sp_Test")] + public int MainResult { get; set; } + + [DynamORM.Mapper.Column("result")] + public int Result { get; set; } + + [DynamORM.Mapper.Column("description")] + public string Description { get; set; } + + [DynamORM.Mapper.Column("status")] + public int Status { get; set; } + } + + public class ProcedureMultiResult : IProcedureResultReader + { + [DynamORM.Mapper.Column("sp_Multi")] + public int MainResult { get; set; } + + [DynamORM.Mapper.Column("status")] + public int Status { get; set; } + + public System.Collections.Generic.List Codes { get; private set; } = new System.Collections.Generic.List(); + public System.Collections.Generic.List States { get; private set; } = new System.Collections.Generic.List(); + + public void ReadResults(IDataReader reader) + { + while (reader.Read()) + Codes.Add(reader.GetString(0)); + + if (reader.NextResult()) + while (reader.Read()) + States.Add(reader.GetInt32(0)); + } + } + + public class ProcedureMultiResultArgs : IProcedureParameters + { + [ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)] + public int Status { get; set; } + } } diff --git a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs index dc43bab..e9392bc 100644 --- a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs +++ b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs @@ -113,5 +113,87 @@ namespace DynamORM.Tests.Procedure Assert.AreEqual("XYZ", p0.Value); } } + + [Test] + public void TestDeclaredResultTypeComesFromContractInterface() + { + Assert.AreEqual(typeof(ProcedureParameterResult), DynamicProcedureResultBinder.GetDeclaredResultType(new ProcedureParameterObject())); + Assert.AreEqual(typeof(ProcedureMultiResult), DynamicProcedureResultBinder.GetDeclaredResultType(new ProcedureMultiResultArgs())); + Assert.IsNull(DynamicProcedureResultBinder.GetDeclaredResultType(new object())); + } + + [Test] + public void TestDeclaredResultPayloadBindingMapsMainAndOutValues() + { + var result = DynamicProcedureResultBinder.BindPayload( + typeof(ProcedureParameterResult), + "sp_Test", + 15, + new System.Collections.Generic.Dictionary + { + { "result", 7 }, + { "description", "done" }, + { "status", 3 } + }) as ProcedureParameterResult; + + Assert.NotNull(result); + Assert.AreEqual(15, result.MainResult); + Assert.AreEqual(7, result.Result); + Assert.AreEqual("done", result.Description); + Assert.AreEqual(3, result.Status); + } + + [Test] + public void TestDeclaredResultReaderCanConsumeMultipleResultSets() + { + using (var reader = new FakeMultiResultDataReader( + 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 } + }))) + { + var result = DynamicProcedureResultBinder.ReadDeclaredResult(typeof(ProcedureMultiResult), reader) as ProcedureMultiResult; + + Assert.NotNull(result); + CollectionAssert.AreEqual(new[] { "A", "B" }, result.Codes); + CollectionAssert.AreEqual(new[] { 10, 20 }, result.States); + } + } + + [Test] + public void TestDeclaredResultPayloadCanAugmentReaderResult() + { + var existing = new ProcedureMultiResult(); + existing.Codes.Add("A"); + existing.States.Add(10); + + var result = DynamicProcedureResultBinder.BindPayload( + typeof(ProcedureMultiResult), + "sp_Multi", + 99, + new System.Collections.Generic.Dictionary + { + { "status", 5 } + }, + existing) as ProcedureMultiResult; + + Assert.AreSame(existing, result); + Assert.AreEqual(99, result.MainResult); + Assert.AreEqual(5, result.Status); + CollectionAssert.AreEqual(new[] { "A" }, result.Codes); + CollectionAssert.AreEqual(new[] { 10 }, result.States); + } } } diff --git a/DynamORM/DynamicProcedureInvoker.cs b/DynamORM/DynamicProcedureInvoker.cs index dff15de..329ca72 100644 --- a/DynamORM/DynamicProcedureInvoker.cs +++ b/DynamORM/DynamicProcedureInvoker.cs @@ -89,15 +89,16 @@ namespace DynamORM public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { // parse the method - CallInfo info = binder.CallInfo; - - // Get generic types - IList types = binder.GetGenericTypeArguments(); - - Dictionary retParams = null; - - using (IDbConnection con = _db.Open()) - using (IDbCommand cmd = con.CreateCommand()) + CallInfo info = binder.CallInfo; + + // Get generic types + IList types = binder.GetGenericTypeArguments() ?? new List(); + Type declaredResultType = null; + + Dictionary retParams = null; + + using (IDbConnection con = _db.Open()) + using (IDbCommand cmd = con.CreateCommand()) { if (_prefixes == null || _prefixes.Count == 0) cmd.SetCommand(CommandType.StoredProcedure, binder.Name); @@ -108,6 +109,7 @@ namespace DynamORM int alen = args.Length; bool retIsAdded = false; + declaredResultType = alen == 1 ? DynamicProcedureResultBinder.GetDeclaredResultType(args[0]) : null; if (alen > 0) { @@ -278,11 +280,17 @@ namespace DynamORM #region Get main result - object mainResult = null; - - if (types.Count > 0) - { - mainResult = types[0].GetDefaultValue(); + object mainResult = null; + + if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType)) + { + using (IDataReader rdr = cmd.ExecuteReader()) + using (IDataReader cache = rdr.CachedReader()) + mainResult = DynamicProcedureResultBinder.ReadDeclaredResult(declaredResultType, cache); + } + else if (types.Count > 0) + { + mainResult = types[0].GetDefaultValue(); if (types[0] == typeof(IDataReader)) { @@ -418,9 +426,9 @@ namespace DynamORM #region Handle out params - if (retParams != null) - { - Dictionary res = new Dictionary(); + if (retParams != null) + { + Dictionary res = new Dictionary(); if (mainResult != null) { @@ -430,23 +438,29 @@ namespace DynamORM res.Add(binder.Name, mainResult); } - foreach (KeyValuePair pos in retParams) - res.Add(pos.Key, ((IDbDataParameter)cmd.Parameters[pos.Value]).Value); - - if (types.Count > 1) - { + foreach (KeyValuePair pos in retParams) + res.Add(pos.Key, ((IDbDataParameter)cmd.Parameters[pos.Value]).Value); + + if (types.Count > 1) + { DynamicTypeMap mapper = DynamicMapperCache.GetMapper(types[1]); if (mapper != null) - result = mapper.Create(res.ToDynamic()); - else - result = res.ToDynamic(); - } - else - result = res.ToDynamic(); - } - else - result = mainResult; + result = mapper.Create(res.ToDynamic()); + else + result = res.ToDynamic(); + } + else if (declaredResultType != null) + result = DynamicProcedureResultBinder.BindPayload(declaredResultType, binder.Name, mainResult, res.Where(x => x.Key != binder.Name).ToDictionary(x => x.Key, x => x.Value), mainResult != null && declaredResultType.IsInstanceOfType(mainResult) ? mainResult : null); + else + result = res.ToDynamic(); + } + else if (declaredResultType != null && mainResult != null && declaredResultType.IsInstanceOfType(mainResult)) + result = mainResult; + else if (declaredResultType != null) + result = DynamicProcedureResultBinder.BindPayload(declaredResultType, binder.Name, mainResult, null); + else + result = mainResult; #endregion Handle out params } diff --git a/DynamORM/Helpers/DynamicProcedureParameterBinder.cs b/DynamORM/Helpers/DynamicProcedureParameterBinder.cs index 65960f6..30cd4d6 100644 --- a/DynamORM/Helpers/DynamicProcedureParameterBinder.cs +++ b/DynamORM/Helpers/DynamicProcedureParameterBinder.cs @@ -45,15 +45,10 @@ namespace DynamORM.Helpers internal static bool CanBind(object item) { - if (item == null) + if (!DynamicProcedureResultBinder.IsProcedureContract(item)) return false; - Type type = item.GetType(); - - if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(Guid) || type == typeof(TimeSpan)) - return false; - - return GetBindableProperties(type).Any(); + return GetBindableProperties(item.GetType()).Any(); } internal static BindingResult Bind(DynamicDatabase db, IDbCommand cmd, object item) diff --git a/DynamORM/Helpers/DynamicProcedureResultBinder.cs b/DynamORM/Helpers/DynamicProcedureResultBinder.cs new file mode 100644 index 0000000..1845dff --- /dev/null +++ b/DynamORM/Helpers/DynamicProcedureResultBinder.cs @@ -0,0 +1,106 @@ +/* + * 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; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using DynamORM.Mapper; + +namespace DynamORM.Helpers +{ + internal static class DynamicProcedureResultBinder + { + internal static bool IsProcedureContract(object item) + { + return item is IProcedureParameters; + } + + internal static Type GetDeclaredResultType(object item) + { + if (item == null) + return null; + + Type iface = item.GetType().GetInterfaces() + .FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IProcedureParameters<>)); + + return iface == null ? null : iface.GetGenericArguments()[0]; + } + + internal static bool CanReadResults(Type resultType) + { + return resultType != null && typeof(IProcedureResultReader).IsAssignableFrom(resultType); + } + + internal static object CreateDeclaredResult(Type resultType) + { + if (resultType == null) + return null; + + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(resultType); + if (mapper != null) + return mapper.Creator(); + + return Activator.CreateInstance(resultType); + } + + 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)); + + object instance = CreateDeclaredResult(resultType); + ((IProcedureResultReader)instance).ReadResults(reader); + return instance; + } + + internal static object BindPayload(Type resultType, string mainResultName, object mainResult, IDictionary returnValues, object existing = null) + { + if (resultType == null) + return existing ?? returnValues.ToDynamic(); + + Dictionary payload = new Dictionary(); + + if (mainResultName != null) + payload[mainResultName] = mainResult == DBNull.Value ? null : mainResult; + + if (returnValues != null) + foreach (KeyValuePair item in returnValues) + payload[item.Key] = item.Value == DBNull.Value ? null : item.Value; + + DynamicTypeMap mapper = DynamicMapperCache.GetMapper(resultType); + if (mapper != null) + return mapper.Map(payload.ToDynamic(), existing ?? mapper.Creator()); + + if (existing != null) + return existing; + + return payload.ToDynamic(); + } + } +} diff --git a/DynamORM/ProcedureContracts.cs b/DynamORM/ProcedureContracts.cs new file mode 100644 index 0000000..b2c32c7 --- /dev/null +++ b/DynamORM/ProcedureContracts.cs @@ -0,0 +1,51 @@ +/* + * 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.Data; + +namespace DynamORM +{ + /// 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); + } +}