From f8353e74888cac5ca430ccbb78586ab421735c9a Mon Sep 17 00:00:00 2001 From: Grzegorz Russek Date: Fri, 27 Feb 2026 16:34:16 +0100 Subject: [PATCH] Expose typed procedure execution as explicit APIs --- AmalgamationTool/DynamORM.Amalgamation.cs | 67 ++++++++++++----- .../ProcedureParameterBinderTests.cs | 10 +-- DynamORM/DynamicDatabase.cs | 38 +++++++--- DynamORM/DynamicProcedureInvoker.cs | 73 +++++++++++-------- 4 files changed, 122 insertions(+), 66 deletions(-) diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index afd2ab7..a9d936f 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -2681,6 +2681,24 @@ namespace DynamORM .ExecuteNonQuery(); } } + /// Execute typed stored procedure descriptor. + /// Procedure descriptor type. + /// Procedure result. + public virtual object Procedure() + { + return Procedure(null); + } + /// Execute typed stored procedure descriptor. + /// Procedure descriptor type. + /// Procedure arguments contract. + /// Procedure result. + public virtual object Procedure(object args) + { + if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures) + throw new InvalidOperationException("Database connection desn't support stored procedures."); + + return new DynamicProcedureInvoker(this).Exec(args); + } #endregion Procedure #region Execute @@ -5331,6 +5349,23 @@ namespace DynamORM _prefixes = prefixes; _db = db; } + /// Execute typed stored procedure descriptor. + /// Procedure descriptor type. + /// Optional procedure arguments contract. + /// Procedure result. + public virtual object Exec(object args = null) + { + DynamicProcedureDescriptor descriptor = DynamicProcedureDescriptor.Resolve(typeof(TProcedure)); + return InvokeProcedure( + descriptor.ProcedureName, + descriptor.ResultName, + new List(), + args == null ? new object[0] : new[] { args }, + new CallInfo(args == null ? 0 : 1), + descriptor.ResultType, + descriptor.ArgumentsType, + descriptor.ProcedureType); + } /// This is where the magic begins. /// Binder to create owner. /// Binder invoke result. @@ -5360,31 +5395,23 @@ namespace DynamORM // Get generic types IList types = binder.GetGenericTypeArguments() ?? new List(); - Type declaredResultType = null; - string procedureName = binder.Name; - string resultName = binder.Name; - Type execArgumentsType = null; - if (binder.Name == "Exec") + result = InvokeProcedure(binder.Name, binder.Name, types, args, info, null, null, null); + return true; + } + internal object InvokeProcedure(string procedureName, string resultName, IList types, object[] args, CallInfo info, Type declaredResultType, Type expectedArgumentsType, Type procedureType) + { + object result; + Dictionary retParams = null; + + if (expectedArgumentsType != null) { - if (types.Count != 1) - throw new InvalidOperationException("Exec(args) requires exactly one generic procedure descriptor type."); - - DynamicProcedureDescriptor descriptor = DynamicProcedureDescriptor.Resolve(types[0]); - procedureName = descriptor.ProcedureName; - resultName = descriptor.ResultName; - execArgumentsType = descriptor.ArgumentsType; - declaredResultType = descriptor.ResultType; - types = new List(); - if (args.Length > 1) throw new InvalidOperationException("Exec(args) accepts at most one arguments contract instance."); - if (args.Length == 1 && args[0] != null && !execArgumentsType.IsAssignableFrom(args[0].GetType())) - throw new InvalidOperationException(string.Format("Exec<{0}>(args) expects argument of type '{1}', received '{2}'.", descriptor.ProcedureType.FullName, execArgumentsType.FullName, args[0].GetType().FullName)); + if (args.Length == 1 && args[0] != null && !expectedArgumentsType.IsAssignableFrom(args[0].GetType())) + throw new InvalidOperationException(string.Format("Exec<{0}>(args) expects argument of type '{1}', received '{2}'.", procedureType == null ? expectedArgumentsType.FullName : procedureType.FullName, expectedArgumentsType.FullName, args[0].GetType().FullName)); } - Dictionary retParams = null; - using (IDbConnection con = _db.Open()) using (IDbCommand cmd = con.CreateCommand()) { @@ -5740,7 +5767,7 @@ namespace DynamORM #endregion Handle out params } - return true; + return result; } /// Performs application-defined tasks associated with /// freeing, releasing, or resetting unmanaged resources. diff --git a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs index a98f2d7..6036f19 100644 --- a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs +++ b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs @@ -151,9 +151,9 @@ namespace DynamORM.Tests.Procedure } [Test] - public void TestExecRejectsWrongArgumentsType() + public void TestExecMethodRejectsWrongArgumentsType() { - dynamic procedures = new DynamicProcedureInvoker(null); + var procedures = new DynamicProcedureInvoker(null); Assert.Throws(() => { @@ -162,13 +162,11 @@ namespace DynamORM.Tests.Procedure } [Test] - public void TestExecRejectsMultipleArguments() + public void TestDynamicDatabaseTypedProcedureRejectsWrongArgumentsType() { - dynamic procedures = new DynamicProcedureInvoker(null); - Assert.Throws(() => { - var ignored = procedures.Exec(new ProcedureParameterObject(), new ProcedureParameterObject()); + var ignored = Database.Procedure(new ProcedureParameterColumnFallbackObject()); }); } diff --git a/DynamORM/DynamicDatabase.cs b/DynamORM/DynamicDatabase.cs index 0735cc5..843b5f9 100644 --- a/DynamORM/DynamicDatabase.cs +++ b/DynamORM/DynamicDatabase.cs @@ -1232,10 +1232,10 @@ namespace DynamORM /// Name of stored procedure to execute. /// Arguments (parameters) in form of expando object. /// Number of affected rows. - public virtual int Procedure(string procName, ExpandoObject args) - { - if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures) - throw new InvalidOperationException("Database connection desn't support stored procedures."); + public virtual int Procedure(string procName, ExpandoObject args) + { + if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures) + throw new InvalidOperationException("Database connection desn't support stored procedures."); using (IDbConnection con = Open()) using (IDbCommand cmd = con.CreateCommand()) @@ -1243,11 +1243,31 @@ namespace DynamORM return cmd .SetCommand(CommandType.StoredProcedure, procName) .AddParameters(this, args) - .ExecuteNonQuery(); - } - } - - #endregion Procedure + .ExecuteNonQuery(); + } + } + + /// Execute typed stored procedure descriptor. + /// Procedure descriptor type. + /// Procedure result. + public virtual object Procedure() + { + return Procedure(null); + } + + /// Execute typed stored procedure descriptor. + /// Procedure descriptor type. + /// Procedure arguments contract. + /// Procedure result. + public virtual object Procedure(object args) + { + if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures) + throw new InvalidOperationException("Database connection desn't support stored procedures."); + + return new DynamicProcedureInvoker(this).Exec(args); + } + + #endregion Procedure #region Execute diff --git a/DynamORM/DynamicProcedureInvoker.cs b/DynamORM/DynamicProcedureInvoker.cs index 068115d..4192bb1 100644 --- a/DynamORM/DynamicProcedureInvoker.cs +++ b/DynamORM/DynamicProcedureInvoker.cs @@ -57,11 +57,29 @@ namespace DynamORM private List _prefixes; private bool _isDisposed; - internal DynamicProcedureInvoker(DynamicDatabase db, List prefixes = null) - { - _prefixes = prefixes; - _db = db; - } + internal DynamicProcedureInvoker(DynamicDatabase db, List prefixes = null) + { + _prefixes = prefixes; + _db = db; + } + + /// Execute typed stored procedure descriptor. + /// Procedure descriptor type. + /// Optional procedure arguments contract. + /// Procedure result. + public virtual object Exec(object args = null) + { + DynamicProcedureDescriptor descriptor = DynamicProcedureDescriptor.Resolve(typeof(TProcedure)); + return InvokeProcedure( + descriptor.ProcedureName, + descriptor.ResultName, + new List(), + args == null ? new object[0] : new[] { args }, + new CallInfo(args == null ? 0 : 1), + descriptor.ResultType, + descriptor.ArgumentsType, + descriptor.ProcedureType); + } /// This is where the magic begins. /// Binder to create owner. @@ -93,35 +111,28 @@ namespace DynamORM // Get generic types IList types = binder.GetGenericTypeArguments() ?? new List(); - Type declaredResultType = null; - string procedureName = binder.Name; - string resultName = binder.Name; - Type execArgumentsType = null; - if (binder.Name == "Exec") + result = InvokeProcedure(binder.Name, binder.Name, types, args, info, null, null, null); + return true; + } + + internal object InvokeProcedure(string procedureName, string resultName, IList types, object[] args, CallInfo info, Type declaredResultType, Type expectedArgumentsType, Type procedureType) + { + object result; + Dictionary retParams = null; + + if (expectedArgumentsType != null) { - if (types.Count != 1) - throw new InvalidOperationException("Exec(args) requires exactly one generic procedure descriptor type."); - - DynamicProcedureDescriptor descriptor = DynamicProcedureDescriptor.Resolve(types[0]); - procedureName = descriptor.ProcedureName; - resultName = descriptor.ResultName; - execArgumentsType = descriptor.ArgumentsType; - declaredResultType = descriptor.ResultType; - types = new List(); - if (args.Length > 1) throw new InvalidOperationException("Exec(args) accepts at most one arguments contract instance."); - if (args.Length == 1 && args[0] != null && !execArgumentsType.IsAssignableFrom(args[0].GetType())) - throw new InvalidOperationException(string.Format("Exec<{0}>(args) expects argument of type '{1}', received '{2}'.", descriptor.ProcedureType.FullName, execArgumentsType.FullName, args[0].GetType().FullName)); + if (args.Length == 1 && args[0] != null && !expectedArgumentsType.IsAssignableFrom(args[0].GetType())) + throw new InvalidOperationException(string.Format("Exec<{0}>(args) expects argument of type '{1}', received '{2}'.", procedureType == null ? expectedArgumentsType.FullName : procedureType.FullName, expectedArgumentsType.FullName, args[0].GetType().FullName)); } - Dictionary retParams = null; - using (IDbConnection con = _db.Open()) using (IDbCommand cmd = con.CreateCommand()) - { + { if (_prefixes == null || _prefixes.Count == 0) cmd.SetCommand(CommandType.StoredProcedure, procedureName); else @@ -484,12 +495,12 @@ namespace DynamORM result = DynamicProcedureResultBinder.BindPayload(declaredResultType, resultName, mainResult, null); else result = mainResult; - - #endregion Handle out params - } - - return true; - } + + #endregion Handle out params + } + + return result; + } /// Performs application-defined tasks associated with /// freeing, releasing, or resetting unmanaged resources.