Merge feature/procedure-parameter-object

This commit is contained in:
2026-02-27 16:52:26 +01:00
18 changed files with 3663 additions and 454 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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;
using System.Collections.Generic;
using System.Data;
namespace DynamORM.Tests.Helpers
{
internal sealed class FakeDbCommand : IDbCommand
{
private readonly FakeParameterCollection _parameters = new FakeParameterCollection();
public string CommandText { get; set; }
public int CommandTimeout { get; set; }
public CommandType CommandType { get; set; }
public IDbConnection Connection { get; set; }
public IDataParameterCollection Parameters { get { return _parameters; } }
public IDbTransaction Transaction { get; set; }
public UpdateRowSource UpdatedRowSource { get; set; }
public void Cancel() { }
public IDbDataParameter CreateParameter() { return new FakeDbParameter(); }
public void Dispose() { }
public int ExecuteNonQuery() { throw new NotSupportedException(); }
public IDataReader ExecuteReader() { throw new NotSupportedException(); }
public IDataReader ExecuteReader(CommandBehavior behavior) { throw new NotSupportedException(); }
public object ExecuteScalar() { throw new NotSupportedException(); }
public void Prepare() { }
}
internal sealed class FakeDbParameter : IDbDataParameter
{
public byte Precision { get; set; }
public byte Scale { get; set; }
public int Size { get; set; }
public DbType DbType { get; set; }
public ParameterDirection Direction { get; set; }
public bool IsNullable { get { return true; } }
public string ParameterName { get; set; }
public string SourceColumn { get; set; }
public DataRowVersion SourceVersion { get; set; }
public object Value { get; set; }
}
internal sealed class FakeParameterCollection : IDataParameterCollection
{
private readonly List<object> _items = new List<object>();
public object this[string parameterName]
{
get { return _items.Find(x => string.Equals(((IDbDataParameter)x).ParameterName, parameterName, StringComparison.Ordinal)); }
set { throw new NotSupportedException(); }
}
public object this[int index]
{
get { return _items[index]; }
set { _items[index] = value; }
}
public bool IsFixedSize { get { return false; } }
public bool IsReadOnly { get { return false; } }
public int Count { get { return _items.Count; } }
public bool IsSynchronized { get { return false; } }
public object SyncRoot { get { return this; } }
public int Add(object value)
{
_items.Add(value);
return _items.Count - 1;
}
public void Clear()
{
_items.Clear();
}
public bool Contains(string parameterName)
{
return _items.Exists(x => string.Equals(((IDbDataParameter)x).ParameterName, parameterName, StringComparison.Ordinal));
}
public bool Contains(object value)
{
return _items.Contains(value);
}
public void CopyTo(Array array, int index)
{
_items.ToArray().CopyTo(array, index);
}
public IEnumerator GetEnumerator()
{
return _items.GetEnumerator();
}
public int IndexOf(string parameterName)
{
return _items.FindIndex(x => string.Equals(((IDbDataParameter)x).ParameterName, parameterName, StringComparison.Ordinal));
}
public int IndexOf(object value)
{
return _items.IndexOf(value);
}
public void Insert(int index, object value)
{
_items.Insert(index, value);
}
public void Remove(object value)
{
_items.Remove(value);
}
public void RemoveAt(string parameterName)
{
int index = IndexOf(parameterName);
if (index >= 0)
_items.RemoveAt(index);
}
public void RemoveAt(int index)
{
_items.RemoveAt(index);
}
}
}

View File

@@ -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<ResultSet> _sets = new List<ResultSet>();
private int _setIndex;
private int _rowIndex = -1;
public FakeMultiResultDataReader(params Tuple<string[], Type[], object[][]>[] 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;
}
}
}

View File

@@ -0,0 +1,156 @@
/*
* DynamORM - Dynamic Object-Relational Mapping library.
* Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
* All rights reserved.
*/
using System.Data;
using DynamORM.Mapper;
using DynamORM.Objects;
namespace DynamORM.Tests.Helpers
{
public class ProcedureParameterObject : IProcedureParameters<ProcedureParameterResult>
{
[ProcedureParameter("code", Order = 2, DbType = DbType.String, Size = 32)]
public string Code { get; set; }
[ProcedureParameter("result", Direction = ParameterDirection.Output, Order = 3, DbType = DbType.Int32)]
public int Result { get; set; }
[ProcedureParameter("description", Direction = ParameterDirection.InputOutput, Order = 4, DbType = DbType.String, Size = 256)]
public string Description { get; set; }
[ProcedureParameter("status", Direction = ParameterDirection.ReturnValue, Order = 1)]
public int Status { get; set; }
}
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 ProcedureParameterAttributeMainResult
{
[ProcedureResult]
public int MainResult { get; set; }
[DynamORM.Mapper.Column("status")]
public int Status { get; set; }
}
public class ProcedureParameterAttributeMainResultField
{
[ProcedureResult(-1)]
public int MainResult;
}
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<string> Codes { get; private set; } = new System.Collections.Generic.List<string>();
public System.Collections.Generic.List<int> States { get; private set; } = new System.Collections.Generic.List<int>();
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 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<string> 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<Users> AllUsers { get; set; }
[ProcedureResult(5, Name = "codes_table")]
public DataTable CodesTable { get; set; }
}
public class ProcedureAttributedResultArgs : IProcedureParameters<ProcedureAttributedResult>
{
[ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)]
public int Status { get; set; }
}
public class ProcedureAttributedFieldResult
{
[ProcedureResult(0)]
public string FirstCode;
[ProcedureResult(1)]
public System.Collections.Generic.List<string> Codes;
[ProcedureResult(2, ColumnName = "State")]
public System.Collections.Generic.IEnumerable<int> States;
[ProcedureResult(3)]
public Users User;
[ProcedureResult(4, Name = "users_table")]
public DataTable UsersTable;
}
public class ProcedureMultiResultArgs : IProcedureParameters<ProcedureMultiResult>
{
[ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)]
public int Status { get; set; }
}
[Procedure(Name = "sp_exec_test", Owner = "dbo")]
public class ExecProcedureDescriptor : Procedure<ProcedureParameterObject>
{
}
public class ExecProcedureDefaultDescriptor : Procedure<ProcedureParameterObject>
{
}
[Procedure(Name = "sp_exec_result")]
public class ExecProcedureDescriptorWithExplicitResult : Procedure<ProcedureParameterColumnFallbackObject, ProcedureAttributedResult>
{
}
}

View File

@@ -0,0 +1,383 @@
/*
* DynamORM - Dynamic Object-Relational Mapping library.
* Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
* All rights reserved.
*/
using System.Data;
using System.Dynamic;
using DynamORM.Helpers;
using DynamORM.Tests.Helpers;
using NUnit.Framework;
namespace DynamORM.Tests.Procedure
{
[TestFixture]
public class ProcedureParameterBinderTests : TestsBase
{
[SetUp]
public void SetUp()
{
CreateTestDatabase();
CreateDynamicDatabase(
DynamicDatabaseOptions.SingleConnection |
DynamicDatabaseOptions.SingleTransaction |
DynamicDatabaseOptions.SupportStoredProcedures |
DynamicDatabaseOptions.SupportStoredProceduresResult |
DynamicDatabaseOptions.SupportSchema);
}
[TearDown]
public void TearDown()
{
DestroyDynamicDatabase();
DestroyTestDatabase();
}
[Test]
public void TestCanBindProcedureParameterObject()
{
Assert.IsTrue(DynamicProcedureParameterBinder.CanBind(new ProcedureParameterObject()));
Assert.IsTrue(DynamicProcedureParameterBinder.CanBind(new ProcedureParameterColumnFallbackObject()));
Assert.IsFalse(DynamicProcedureParameterBinder.CanBind("x"));
Assert.IsFalse(DynamicProcedureParameterBinder.CanBind(5));
}
[Test]
public void TestBindUsesAttributeMetadataAndOrder()
{
using (IDbCommand cmd = new FakeDbCommand())
{
var result = DynamicProcedureParameterBinder.Bind(Database, cmd, new ProcedureParameterObject
{
Code = "ABC",
Description = "seed"
});
Assert.IsTrue(result.ReturnValueAdded);
Assert.NotNull(result.ReturnParameters);
Assert.AreEqual(3, result.ReturnParameters.Count);
Assert.AreEqual(4, cmd.Parameters.Count);
var p0 = (IDbDataParameter)cmd.Parameters[0];
var p1 = (IDbDataParameter)cmd.Parameters[1];
var p2 = (IDbDataParameter)cmd.Parameters[2];
var p3 = (IDbDataParameter)cmd.Parameters[3];
Assert.AreEqual(Database.GetParameterName("status"), p0.ParameterName);
Assert.AreEqual(ParameterDirection.ReturnValue, p0.Direction);
Assert.AreEqual(DbType.Int32, p0.DbType);
Assert.AreEqual(4, p0.Size);
Assert.AreEqual(Database.GetParameterName("code"), p1.ParameterName);
Assert.AreEqual(ParameterDirection.Input, p1.Direction);
Assert.AreEqual(DbType.String, p1.DbType);
Assert.AreEqual(32, p1.Size);
Assert.AreEqual("ABC", p1.Value);
Assert.AreEqual(Database.GetParameterName("result"), p2.ParameterName);
Assert.AreEqual(ParameterDirection.Output, p2.Direction);
Assert.AreEqual(DbType.Int32, p2.DbType);
Assert.AreEqual(DBNull.Value, p2.Value);
Assert.AreEqual(Database.GetParameterName("description"), p3.ParameterName);
Assert.AreEqual(ParameterDirection.InputOutput, p3.Direction);
Assert.AreEqual(DbType.String, p3.DbType);
Assert.AreEqual(256, p3.Size);
Assert.AreEqual("seed", p3.Value);
Assert.AreEqual(0, result.ReturnParameters["status"]);
Assert.AreEqual(2, result.ReturnParameters["result"]);
Assert.AreEqual(3, result.ReturnParameters["description"]);
}
}
[Test]
public void TestBindFallsBackToColumnAttributeMetadata()
{
using (IDbCommand cmd = new FakeDbCommand())
{
var result = DynamicProcedureParameterBinder.Bind(Database, cmd, new ProcedureParameterColumnFallbackObject
{
Code = "XYZ"
});
Assert.IsFalse(result.ReturnValueAdded);
Assert.IsNull(result.ReturnParameters);
Assert.AreEqual(1, cmd.Parameters.Count);
var p0 = (IDbDataParameter)cmd.Parameters[0];
Assert.AreEqual(Database.GetParameterName("code"), p0.ParameterName);
Assert.AreEqual(ParameterDirection.Input, p0.Direction);
Assert.AreEqual(DbType.String, p0.DbType);
Assert.AreEqual(64, p0.Size);
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 TestProcedureDescriptorResolvesAttributeNameAndArguments()
{
var descriptor = DynamicProcedureDescriptor.Resolve(typeof(ExecProcedureDescriptor));
Assert.AreEqual(typeof(ExecProcedureDescriptor), descriptor.ProcedureType);
Assert.AreEqual(typeof(ProcedureParameterObject), descriptor.ArgumentsType);
Assert.IsNull(descriptor.ResultType);
Assert.AreEqual("dbo.sp_exec_test", descriptor.ProcedureName);
Assert.AreEqual("sp_exec_test", descriptor.ResultName);
}
[Test]
public void TestProcedureDescriptorResolvesDefaultNameAndExplicitResult()
{
var defaultDescriptor = DynamicProcedureDescriptor.Resolve(typeof(ExecProcedureDefaultDescriptor));
var explicitDescriptor = DynamicProcedureDescriptor.Resolve(typeof(ExecProcedureDescriptorWithExplicitResult));
Assert.AreEqual("ExecProcedureDefaultDescriptor", defaultDescriptor.ProcedureName);
Assert.AreEqual(typeof(ProcedureParameterObject), defaultDescriptor.ArgumentsType);
Assert.IsNull(defaultDescriptor.ResultType);
Assert.AreEqual("sp_exec_result", explicitDescriptor.ProcedureName);
Assert.AreEqual(typeof(ProcedureParameterColumnFallbackObject), explicitDescriptor.ArgumentsType);
Assert.AreEqual(typeof(ProcedureAttributedResult), explicitDescriptor.ResultType);
}
[Test]
public void TestExecMethodRejectsWrongArgumentsType()
{
var procedures = new DynamicProcedureInvoker(null);
Assert.Throws<System.InvalidOperationException>(() =>
{
var ignored = procedures.Exec<ExecProcedureDescriptor>(new ProcedureParameterColumnFallbackObject());
});
}
[Test]
public void TestDynamicDatabaseTypedProcedureRejectsWrongArgumentsType()
{
Assert.Throws<System.InvalidOperationException>(() =>
{
var ignored = Database.Procedure<ExecProcedureDescriptor>(new ProcedureParameterColumnFallbackObject());
});
}
[Test]
public void TestExecTypedOverloadRejectsWrongArgumentsType()
{
var procedures = new DynamicProcedureInvoker(null);
Assert.Throws<System.InvalidOperationException>(() =>
{
var ignored = procedures.Exec<ExecProcedureDescriptorWithExplicitResult, ProcedureAttributedResult>(new ProcedureParameterObject());
});
}
[Test]
public void TestTypedProcedureHandleRejectsWrongArgumentsType()
{
var procedures = new DynamicProcedureInvoker(null);
Assert.Throws<System.InvalidOperationException>(() =>
{
var ignored = procedures.Typed<ExecProcedureDescriptorWithExplicitResult, ProcedureAttributedResult>()
.Exec(new ProcedureParameterObject());
});
}
[Test]
public void TestDynamicDatabaseTypedProcedureHandleRejectsWrongArgumentsType()
{
Assert.Throws<System.InvalidOperationException>(() =>
{
var ignored = Database.TypedProcedure<ExecProcedureDescriptorWithExplicitResult, ProcedureAttributedResult>()
.Exec(new ProcedureParameterObject());
});
}
[Test]
public void TestDeclaredResultPayloadBindingMapsMainAndOutValues()
{
var result = DynamicProcedureResultBinder.BindPayload(
typeof(ProcedureParameterResult),
"sp_Test",
15,
new System.Collections.Generic.Dictionary<string, object>
{
{ "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 TestDeclaredResultPayloadBindingSupportsProcedureResultMainResultProperty()
{
var result = DynamicProcedureResultBinder.BindPayload(
typeof(ProcedureParameterAttributeMainResult),
"sp_Test",
27,
new System.Collections.Generic.Dictionary<string, object>
{
{ "status", 6 }
}) as ProcedureParameterAttributeMainResult;
Assert.NotNull(result);
Assert.AreEqual(27, result.MainResult);
Assert.AreEqual(6, result.Status);
}
[Test]
public void TestDeclaredResultPayloadBindingSupportsProcedureResultMainResultField()
{
var result = DynamicProcedureResultBinder.BindPayload(
typeof(ProcedureParameterAttributeMainResultField),
"sp_Test",
33,
null) as ProcedureParameterAttributeMainResultField;
Assert.NotNull(result);
Assert.AreEqual(33, result.MainResult);
}
[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 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 TestDeclaredResultCanReadAttributedFieldResultSets()
{
using (var reader = new FakeMultiResultDataReader(
Tuple.Create(new[] { "Code" }, new[] { typeof(string) }, new[] { new object[] { "FIELD-FIRST" }, new object[] { "FIELD-SECOND" } }),
Tuple.Create(new[] { "Code" }, new[] { typeof(string) }, new[] { new object[] { "C" }, new object[] { "D" } }),
Tuple.Create(new[] { "State" }, new[] { typeof(int) }, new[] { new object[] { 30 }, new object[] { 40 } }),
Tuple.Create(
new[] { "id", "code", "first" },
new[] { typeof(long), typeof(string), typeof(string) },
new[] { new object[] { 4L, "U4", "Four" } }),
Tuple.Create(
new[] { "id", "code", "first" },
new[] { typeof(long), typeof(string), typeof(string) },
new[] { new object[] { 5L, "U5", "Five" }, new object[] { 6L, "U6", "Six" } })))
{
var result = DynamicProcedureResultBinder.ReadDeclaredResult(typeof(ProcedureAttributedFieldResult), reader) as ProcedureAttributedFieldResult;
Assert.NotNull(result);
Assert.AreEqual("FIELD-FIRST", result.FirstCode);
CollectionAssert.AreEqual(new[] { "C", "D" }, result.Codes);
CollectionAssert.AreEqual(new[] { 30, 40 }, result.States);
Assert.NotNull(result.User);
Assert.AreEqual(4L, result.User.Id);
Assert.NotNull(result.UsersTable);
Assert.AreEqual("users_table", result.UsersTable.TableName);
Assert.AreEqual(2, result.UsersTable.Rows.Count);
}
}
[Test]
public void TestDeclaredResultPayloadCanAugmentReaderResult()
{
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<string, object>
{
{ "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);
}
[Test]
public void TestDeclaredResultBindingDetectionSupportsAttributedResults()
{
Assert.IsTrue(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureMultiResult)));
Assert.IsTrue(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureAttributedResult)));
Assert.IsTrue(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureAttributedFieldResult)));
Assert.IsFalse(DynamicProcedureResultBinder.CanReadResults(typeof(ProcedureParameterResult)));
}
}
}

View File

@@ -38,8 +38,9 @@ using System.Text;
using DynamORM.Builders;
using DynamORM.Builders.Extensions;
using DynamORM.Builders.Implementation;
using DynamORM.Helpers;
using DynamORM.Mapper;
using DynamORM.Helpers;
using DynamORM.Mapper;
using DynamORM.Objects;
namespace DynamORM
{
@@ -1232,10 +1233,10 @@ namespace DynamORM
/// <param name="procName">Name of stored procedure to execute.</param>
/// <param name="args">Arguments (parameters) in form of expando object.</param>
/// <returns>Number of affected rows.</returns>
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 +1244,80 @@ namespace DynamORM
return cmd
.SetCommand(CommandType.StoredProcedure, procName)
.AddParameters(this, args)
.ExecuteNonQuery();
}
}
#endregion Procedure
.ExecuteNonQuery();
}
}
/// <summary>Execute typed stored procedure descriptor.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <returns>Procedure result.</returns>
public virtual object Procedure<TProcedure>()
{
return Procedure<TProcedure>(null);
}
/// <summary>Execute typed stored procedure descriptor.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <param name="args">Procedure arguments contract.</param>
/// <returns>Procedure result.</returns>
public virtual object Procedure<TProcedure>(object args)
{
if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
throw new InvalidOperationException("Database connection desn't support stored procedures.");
return new DynamicProcedureInvoker(this).Exec<TProcedure>(args);
}
/// <summary>Execute typed stored procedure descriptor with strong result type.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <typeparam name="TResult">Procedure result type.</typeparam>
/// <returns>Procedure result.</returns>
public virtual TResult Procedure<TProcedure, TResult>()
where TProcedure : IProcedureDescriptor<TResult>
{
return Procedure<TProcedure, TResult>(null);
}
/// <summary>Execute typed stored procedure descriptor with strong result type.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <typeparam name="TResult">Procedure result type.</typeparam>
/// <param name="args">Procedure arguments contract.</param>
/// <returns>Procedure result.</returns>
public virtual TResult Procedure<TProcedure, TResult>(object args)
where TProcedure : IProcedureDescriptor<TResult>
{
if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
throw new InvalidOperationException("Database connection desn't support stored procedures.");
return new DynamicProcedureInvoker(this).Exec<TProcedure, TResult>(args);
}
/// <summary>Create typed stored procedure execution handle.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <returns>Typed execution handle.</returns>
public virtual TypedProcedureCall<TProcedure> TypedProcedure<TProcedure>()
where TProcedure : IProcedureDescriptor
{
if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
throw new InvalidOperationException("Database connection desn't support stored procedures.");
return new TypedProcedureCall<TProcedure>(new DynamicProcedureInvoker(this));
}
/// <summary>Create typed stored procedure execution handle.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <typeparam name="TResult">Procedure result type.</typeparam>
/// <returns>Typed execution handle.</returns>
public virtual TypedProcedureCall<TProcedure, TResult> TypedProcedure<TProcedure, TResult>()
where TProcedure : IProcedureDescriptor<TResult>
{
if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
throw new InvalidOperationException("Database connection desn't support stored procedures.");
return new TypedProcedureCall<TProcedure, TResult>(new DynamicProcedureInvoker(this));
}
#endregion Procedure
#region Execute

View File

@@ -31,13 +31,14 @@ using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Dynamic;
using System.Linq;
using DynamORM.Helpers;
using DynamORM.Mapper;
namespace DynamORM
{
using System.Dynamic;
using System.Linq;
using DynamORM.Helpers;
using DynamORM.Mapper;
using DynamORM.Objects;
namespace DynamORM
{
/// <summary>Dynamic procedure invoker.</summary>
/// <remarks>Unfortunately I can use <c>out</c> and <c>ref</c> to
/// return parameters, <see href="http://stackoverflow.com/questions/2475310/c-sharp-4-0-dynamic-doesnt-set-ref-out-arguments"/>.
@@ -57,11 +58,59 @@ namespace DynamORM
private List<string> _prefixes;
private bool _isDisposed;
internal DynamicProcedureInvoker(DynamicDatabase db, List<string> prefixes = null)
{
_prefixes = prefixes;
_db = db;
}
internal DynamicProcedureInvoker(DynamicDatabase db, List<string> prefixes = null)
{
_prefixes = prefixes;
_db = db;
}
/// <summary>Execute typed stored procedure descriptor.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <param name="args">Optional procedure arguments contract.</param>
/// <returns>Procedure result.</returns>
public virtual object Exec<TProcedure>(object args = null)
{
DynamicProcedureDescriptor descriptor = DynamicProcedureDescriptor.Resolve(typeof(TProcedure));
return InvokeProcedure(
descriptor.ProcedureName,
descriptor.ResultName,
new List<Type>(),
args == null ? new object[0] : new[] { args },
new CallInfo(args == null ? 0 : 1),
descriptor.ResultType,
descriptor.ArgumentsType,
descriptor.ProcedureType);
}
/// <summary>Execute typed stored procedure descriptor with strong result type.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <typeparam name="TResult">Procedure result type.</typeparam>
/// <param name="args">Optional procedure arguments contract.</param>
/// <returns>Procedure result.</returns>
public virtual TResult Exec<TProcedure, TResult>(object args = null)
where TProcedure : IProcedureDescriptor<TResult>
{
return ConvertProcedureResult<TResult>(Exec<TProcedure>(args));
}
/// <summary>Create typed stored procedure execution handle.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <returns>Typed execution handle.</returns>
public virtual TypedProcedureCall<TProcedure> Typed<TProcedure>()
where TProcedure : IProcedureDescriptor
{
return new TypedProcedureCall<TProcedure>(this);
}
/// <summary>Create typed stored procedure execution handle.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <typeparam name="TResult">Procedure result type.</typeparam>
/// <returns>Typed execution handle.</returns>
public virtual TypedProcedureCall<TProcedure, TResult> Typed<TProcedure, TResult>()
where TProcedure : IProcedureDescriptor<TResult>
{
return new TypedProcedureCall<TProcedure, TResult>(this);
}
/// <summary>This is where the magic begins.</summary>
/// <param name="binder">Binder to create owner.</param>
@@ -86,194 +135,227 @@ namespace DynamORM
/// <param name="args">Binder arguments.</param>
/// <param name="result">Binder invoke result.</param>
/// <returns>Returns <c>true</c> if invoke was performed.</returns>
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
// parse the method
CallInfo info = binder.CallInfo;
// Get generic types
IList<Type> types = binder.GetGenericTypeArguments();
Dictionary<string, int> retParams = null;
using (IDbConnection con = _db.Open())
using (IDbCommand cmd = con.CreateCommand())
{
if (_prefixes == null || _prefixes.Count == 0)
cmd.SetCommand(CommandType.StoredProcedure, binder.Name);
else
cmd.SetCommand(CommandType.StoredProcedure, string.Format("{0}.{1}", string.Join(".", _prefixes), binder.Name));
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
// parse the method
CallInfo info = binder.CallInfo;
// Get generic types
IList<Type> types = binder.GetGenericTypeArguments() ?? new List<Type>();
result = InvokeProcedure(binder.Name, binder.Name, types, args, info, null, null, null);
return true;
}
internal object InvokeProcedure(string procedureName, string resultName, IList<Type> types, object[] args, CallInfo info, Type declaredResultType, Type expectedArgumentsType, Type procedureType)
{
object result;
Dictionary<string, int> retParams = null;
if (expectedArgumentsType != null)
{
if (args.Length > 1)
throw new InvalidOperationException("Exec<TProcedure>(args) accepts at most one arguments contract instance.");
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));
}
using (IDbConnection con = _db.Open())
using (IDbCommand cmd = con.CreateCommand())
{
if (_prefixes == null || _prefixes.Count == 0)
cmd.SetCommand(CommandType.StoredProcedure, procedureName);
else
cmd.SetCommand(CommandType.StoredProcedure, string.Format("{0}.{1}", string.Join(".", _prefixes), procedureName));
#region Prepare arguments
int alen = args.Length;
bool retIsAdded = false;
if (alen > 0)
{
for (int i = 0; i < alen; i++)
{
object arg = args[i];
if (arg is DynamicExpando)
cmd.AddParameters(_db, (DynamicExpando)arg);
else if (arg is ExpandoObject)
cmd.AddParameters(_db, (ExpandoObject)arg);
else if (arg is DynamicColumn)
{
var dcv = (DynamicColumn)arg;
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
if (info.ArgumentNames.Count > i)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
}
paramName = dcv.Alias ?? dcv.ColumnName ??
(dcv.Schema.HasValue ? dcv.Schema.Value.Name : null) ??
paramName;
if (!isOut && !isRet && !isBoth)
{
isOut = dcv.ParameterDirection == ParameterDirection.Output;
isRet = dcv.ParameterDirection == ParameterDirection.ReturnValue;
isBoth = dcv.ParameterDirection == ParameterDirection.InputOutput;
}
if (isRet)
retIsAdded = true;
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
if (dcv.Schema != null)
{
var ds = dcv.Schema.Value;
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
ds.Type, ds.Size, ds.Precision, ds.Scale,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
else
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
else if (arg is DynamicSchemaColumn)
{
var dsc = (DynamicSchemaColumn)arg;
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
if (info.ArgumentNames.Count > i)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
}
paramName = dsc.Name ?? paramName;
if (isRet)
retIsAdded = true;
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
dsc.Type, dsc.Size, dsc.Precision, dsc.Scale,
DBNull.Value);
}
else
{
if (info.ArgumentNames.Count > i && !string.IsNullOrEmpty(info.ArgumentNames[i]))
{
bool isOut = info.ArgumentNames[i].StartsWith("out_");
bool isRet = info.ArgumentNames[i].StartsWith("ret_");
bool isBoth = info.ArgumentNames[i].StartsWith("both_");
if (isRet)
retIsAdded = true;
string paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
if (isOut || isBoth || isRet)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? isRet ? DbType.Int32 : DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : arg);
}
else
cmd.AddParameter(_db, arg);
}
}
}
int alen = args.Length;
bool retIsAdded = false;
if (declaredResultType == null)
declaredResultType = alen == 1 ? DynamicProcedureResultBinder.GetDeclaredResultType(args[0]) : null;
if (alen > 0)
{
if (alen == 1 && DynamicProcedureParameterBinder.CanBind(args[0]))
{
DynamicProcedureParameterBinder.BindingResult bindingResult = DynamicProcedureParameterBinder.Bind(_db, cmd, args[0]);
retParams = bindingResult.ReturnParameters;
retIsAdded = bindingResult.ReturnValueAdded;
}
else
{
for (int i = 0; i < alen; i++)
{
object arg = args[i];
if (arg is DynamicExpando)
cmd.AddParameters(_db, (DynamicExpando)arg);
else if (arg is ExpandoObject)
cmd.AddParameters(_db, (ExpandoObject)arg);
else if (arg is DynamicColumn)
{
var dcv = (DynamicColumn)arg;
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
if (info.ArgumentNames.Count > i)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
}
paramName = dcv.Alias ?? dcv.ColumnName ??
(dcv.Schema.HasValue ? dcv.Schema.Value.Name : null) ??
paramName;
if (!isOut && !isRet && !isBoth)
{
isOut = dcv.ParameterDirection == ParameterDirection.Output;
isRet = dcv.ParameterDirection == ParameterDirection.ReturnValue;
isBoth = dcv.ParameterDirection == ParameterDirection.InputOutput;
}
if (isRet)
retIsAdded = true;
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
if (dcv.Schema != null)
{
var ds = dcv.Schema.Value;
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
ds.Type, ds.Size, ds.Precision, ds.Scale,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
else
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : dcv.Value);
}
else if (arg is DynamicSchemaColumn)
{
var dsc = (DynamicSchemaColumn)arg;
string paramName = i.ToString();
bool isOut = false;
bool isRet = false;
bool isBoth = false;
if (info.ArgumentNames.Count > i)
{
isOut = info.ArgumentNames[i].StartsWith("out_");
isRet = info.ArgumentNames[i].StartsWith("ret_");
isBoth = info.ArgumentNames[i].StartsWith("both_");
paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
}
paramName = dsc.Name ?? paramName;
if (isRet)
retIsAdded = true;
if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
dsc.Type, dsc.Size, dsc.Precision, dsc.Scale,
DBNull.Value);
}
else
{
if (info.ArgumentNames.Count > i && !string.IsNullOrEmpty(info.ArgumentNames[i]))
{
bool isOut = info.ArgumentNames[i].StartsWith("out_");
bool isRet = info.ArgumentNames[i].StartsWith("ret_");
bool isBoth = info.ArgumentNames[i].StartsWith("both_");
if (isRet)
retIsAdded = true;
string paramName = isOut || isRet ?
info.ArgumentNames[i].Substring(4) :
isBoth ? info.ArgumentNames[i].Substring(5) :
info.ArgumentNames[i];
if (isOut || isBoth || isRet)
{
if (retParams == null)
retParams = new Dictionary<string, int>();
retParams.Add(paramName, cmd.Parameters.Count);
}
cmd.AddParameter(
_db.GetParameterName(paramName),
isOut ? ParameterDirection.Output :
isRet ? ParameterDirection.ReturnValue :
isBoth ? ParameterDirection.InputOutput :
ParameterDirection.Input,
arg == null ? isRet ? DbType.Int32 : DbType.String : arg.GetType().ToDbType(),
isRet ? 4 : 0,
(isOut || isRet) ? DBNull.Value : arg);
}
else
cmd.AddParameter(_db, arg);
}
}
}
}
#endregion Prepare arguments
#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))
{
@@ -284,7 +366,7 @@ namespace DynamORM
{
using (IDataReader rdr = cmd.ExecuteReader())
using (IDataReader cache = rdr.CachedReader())
mainResult = cache.ToDataTable(binder.Name);
mainResult = cache.ToDataTable(resultName);
}
else if (types[0].IsGenericEnumerable())
{
@@ -409,41 +491,62 @@ namespace DynamORM
#region Handle out params
if (retParams != null)
{
Dictionary<string, object> res = new Dictionary<string, object>();
if (retParams != null)
{
Dictionary<string, object> res = new Dictionary<string, object>();
if (mainResult != null)
{
if (mainResult == DBNull.Value)
res.Add(binder.Name, null);
else
res.Add(binder.Name, mainResult);
}
res.Add(resultName, null);
else
res.Add(resultName, mainResult);
}
foreach (KeyValuePair<string, int> pos in retParams)
res.Add(pos.Key, ((IDbDataParameter)cmd.Parameters[pos.Value]).Value);
if (types.Count > 1)
{
foreach (KeyValuePair<string, int> 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;
#endregion Handle out params
}
return true;
}
result = mapper.Create(res.ToDynamic());
else
result = res.ToDynamic();
}
else if (declaredResultType != null)
result = DynamicProcedureResultBinder.BindPayload(declaredResultType, resultName, mainResult, res.Where(x => x.Key != resultName).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, resultName, mainResult, null);
else
result = mainResult;
#endregion Handle out params
}
return result;
}
private static TResult ConvertProcedureResult<TResult>(object result)
{
if (result == null || result == DBNull.Value)
return default(TResult);
if (result is TResult)
return (TResult)result;
DynamicTypeMap mapper = DynamicMapperCache.GetMapper(typeof(TResult));
if (mapper != null)
return (TResult)DynamicExtensions.Map(result, typeof(TResult));
return (TResult)typeof(TResult).CastObject(result);
}
/// <summary>Performs application-defined tasks associated with
/// freeing, releasing, or resetting unmanaged resources.</summary>

View File

@@ -0,0 +1,100 @@
/*
* 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.Linq;
using DynamORM.Mapper;
using DynamORM.Objects;
namespace DynamORM.Helpers
{
internal sealed class DynamicProcedureDescriptor
{
public Type ProcedureType { get; set; }
public Type ArgumentsType { get; set; }
public Type ResultType { get; set; }
public string ProcedureName { get; set; }
public string ResultName { get; set; }
internal static DynamicProcedureDescriptor Resolve(Type procedureType)
{
if (procedureType == null)
throw new ArgumentNullException("procedureType");
Type current = procedureType;
Type argumentsType = null;
Type resultType = null;
while (current != null && current != typeof(object))
{
if (current.IsGenericType)
{
Type genericDefinition = current.GetGenericTypeDefinition();
Type[] genericArguments = current.GetGenericArguments();
if (genericDefinition == typeof(Procedure<>) && genericArguments.Length == 1)
{
argumentsType = genericArguments[0];
break;
}
if (genericDefinition == typeof(Procedure<,>) && genericArguments.Length == 2)
{
argumentsType = genericArguments[0];
resultType = genericArguments[1];
break;
}
}
current = current.BaseType;
}
if (argumentsType == null)
throw new InvalidOperationException(string.Format("Type '{0}' is not a typed procedure descriptor.", procedureType.FullName));
if (!typeof(IProcedureParameters).IsAssignableFrom(argumentsType))
throw new InvalidOperationException(string.Format("Procedure descriptor '{0}' declares argument type '{1}' that does not implement IProcedureParameters.", procedureType.FullName, argumentsType.FullName));
ProcedureAttribute attr = procedureType.GetCustomAttributes(typeof(ProcedureAttribute), true)
.Cast<ProcedureAttribute>()
.FirstOrDefault();
string name = attr != null && !string.IsNullOrEmpty(attr.Name) ? attr.Name : procedureType.Name;
string owner = attr != null ? attr.Owner : null;
return new DynamicProcedureDescriptor
{
ProcedureType = procedureType,
ArgumentsType = argumentsType,
ResultType = resultType,
ProcedureName = string.IsNullOrEmpty(owner) ? name : string.Format("{0}.{1}", owner, name),
ResultName = name
};
}
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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 System.Reflection;
using DynamORM.Mapper;
namespace DynamORM.Helpers
{
internal static class DynamicProcedureParameterBinder
{
internal sealed class BindingResult
{
public bool ReturnValueAdded { get; set; }
public Dictionary<string, int> ReturnParameters { get; set; }
}
internal static bool CanBind(object item)
{
if (!DynamicProcedureResultBinder.IsProcedureContract(item))
return false;
return GetBindableProperties(item.GetType()).Any();
}
internal static BindingResult Bind(DynamicDatabase db, IDbCommand cmd, object item)
{
if (db == null)
throw new ArgumentNullException("db");
if (cmd == null)
throw new ArgumentNullException("cmd");
if (item == null)
throw new ArgumentNullException("item");
BindingResult result = new BindingResult();
foreach (PropertyInfo property in GetBindableProperties(item.GetType()))
{
ProcedureParameterAttribute procAttr = property.GetCustomAttributes(typeof(ProcedureParameterAttribute), true).Cast<ProcedureParameterAttribute>().FirstOrDefault();
ColumnAttribute colAttr = property.GetCustomAttributes(typeof(ColumnAttribute), true).Cast<ColumnAttribute>().FirstOrDefault();
string name = (procAttr != null && !string.IsNullOrEmpty(procAttr.Name) ? procAttr.Name : null)
?? (colAttr != null && !string.IsNullOrEmpty(colAttr.Name) ? colAttr.Name : null)
?? property.Name;
ParameterDirection direction = procAttr == null ? ParameterDirection.Input : procAttr.Direction;
object value = property.GetValue(item, null);
DbType dbType = ResolveDbType(property.PropertyType, value, procAttr, colAttr, direction);
int size = ResolveSize(dbType, value, procAttr, colAttr, direction);
byte precision = (procAttr != null && procAttr.Precision != ProcedureParameterAttribute.UnspecifiedByte ? procAttr.Precision : default(byte));
byte scale = (procAttr != null && procAttr.Scale != ProcedureParameterAttribute.UnspecifiedByte ? procAttr.Scale : default(byte));
if (procAttr == null && colAttr != null)
{
precision = colAttr.Precision ?? precision;
scale = colAttr.Scale ?? scale;
}
if (direction == ParameterDirection.ReturnValue)
result.ReturnValueAdded = true;
if (direction == ParameterDirection.Output || direction == ParameterDirection.InputOutput || direction == ParameterDirection.ReturnValue)
{
if (result.ReturnParameters == null)
result.ReturnParameters = new Dictionary<string, int>();
result.ReturnParameters.Add(name, cmd.Parameters.Count);
}
cmd.AddParameter(
db.GetParameterName(name),
direction,
dbType,
size,
precision,
scale,
direction == ParameterDirection.Output || direction == ParameterDirection.ReturnValue ? DBNull.Value : (value ?? DBNull.Value));
}
return result;
}
private static IEnumerable<PropertyInfo> GetBindableProperties(Type type)
{
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanRead && x.GetIndexParameters().Length == 0)
.Where(x => x.GetCustomAttributes(typeof(ProcedureParameterAttribute), true).Any() || x.GetCustomAttributes(typeof(ColumnAttribute), true).Any())
.OrderBy(x =>
{
ProcedureParameterAttribute attr = x.GetCustomAttributes(typeof(ProcedureParameterAttribute), true).Cast<ProcedureParameterAttribute>().FirstOrDefault();
return attr == null ? int.MaxValue : attr.Order;
})
.ThenBy(x => x.MetadataToken);
}
private static DbType ResolveDbType(Type propertyType, object value, ProcedureParameterAttribute procAttr, ColumnAttribute colAttr, ParameterDirection direction)
{
if (procAttr != null && (int)procAttr.DbType != ProcedureParameterAttribute.UnspecifiedDbType)
return procAttr.DbType;
if (colAttr != null && colAttr.Type.HasValue)
return colAttr.Type.Value;
Type targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
if (targetType == typeof(object) && value != null)
targetType = value.GetType();
if (value == null && direction == ParameterDirection.ReturnValue)
return DbType.Int32;
return targetType.ToDbType();
}
private static int ResolveSize(DbType dbType, object value, ProcedureParameterAttribute procAttr, ColumnAttribute colAttr, ParameterDirection direction)
{
if (procAttr != null && procAttr.Size != ProcedureParameterAttribute.UnspecifiedSize)
return procAttr.Size;
if (colAttr != null && colAttr.Size.HasValue)
return colAttr.Size.Value;
if (direction == ParameterDirection.ReturnValue)
return 4;
if (dbType == DbType.AnsiString || dbType == DbType.AnsiStringFixedLength)
{
if (value != null)
return value.ToString().Length > 8000 ? -1 : 8000;
return 8000;
}
if (dbType == DbType.String || dbType == DbType.StringFixedLength)
{
if (value != null)
return value.ToString().Length > 4000 ? -1 : 4000;
return 4000;
}
return 0;
}
}
}

View File

@@ -0,0 +1,452 @@
/*
* 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 System.Reflection;
using DynamORM.Mapper;
using DynamORM.Objects;
namespace DynamORM.Helpers
{
internal static class DynamicProcedureResultBinder
{
private sealed class ResultMemberBinding
{
public ProcedureResultAttribute Attribute { get; set; }
public MemberInfo Member { get; set; }
public Type MemberType { get; set; }
public int SortOrder { get; set; }
public void SetValue(object instance, object value)
{
PropertyInfo property = Member as PropertyInfo;
if (property != null)
{
property.SetValue(instance, value, null);
return;
}
FieldInfo field = Member as FieldInfo;
if (field != null)
field.SetValue(instance, value);
}
}
internal static bool IsProcedureContract(object item)
{
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) || GetResultMemberBindings(resultType).Count > 0);
}
internal static bool HasDeclaredResultBinding(Type resultType)
{
return CanReadResults(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 declare a supported procedure result binding.", resultType == null ? "<null>" : resultType.FullName));
object instance = CreateDeclaredResult(resultType);
IList<ResultMemberBinding> bindings = GetResultMemberBindings(resultType);
if (bindings.Count > 0)
BindResultMembers(instance, reader, bindings);
else
((IProcedureResultReader)instance).ReadResults(reader);
return instance;
}
internal static object BindPayload(Type resultType, string mainResultName, object mainResult, IDictionary<string, object> returnValues, object existing = null)
{
if (resultType == null)
return existing ?? returnValues.ToDynamic();
Dictionary<string, object> payload = new Dictionary<string, object>();
if (mainResultName != null)
payload[mainResultName] = mainResult == DBNull.Value ? null : mainResult;
if (returnValues != null)
foreach (KeyValuePair<string, object> item in returnValues)
payload[item.Key] = item.Value == DBNull.Value ? null : item.Value;
DynamicTypeMap mapper = DynamicMapperCache.GetMapper(resultType);
object instance = existing;
if (mapper != null)
instance = mapper.Map(payload.ToDynamic(), existing ?? mapper.Creator());
else if (instance == null)
instance = payload.ToDynamic();
if (instance != null && resultType.IsInstanceOfType(instance))
BindMainResultMembers(instance, mainResult);
return instance;
}
private static IList<ResultMemberBinding> GetResultMemberBindings(Type resultType)
{
var properties = resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanWrite && x.GetIndexParameters().Length == 0)
.Select(x => new ResultMemberBinding
{
Member = x,
MemberType = x.PropertyType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
});
var fields = resultType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Where(x => !x.IsInitOnly && !x.IsLiteral)
.Select(x => new ResultMemberBinding
{
Member = x,
MemberType = x.FieldType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
});
return properties.Concat(fields)
.Where(x => x.Attribute != null)
.Where(x => !IsMainResultBinding(x.Attribute))
.OrderBy(x => x.Attribute.ResultIndex)
.ThenBy(x => x.SortOrder)
.ToList();
}
private static IList<ResultMemberBinding> GetMainResultBindings(Type resultType)
{
var properties = resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.CanWrite && x.GetIndexParameters().Length == 0)
.Select(x => new ResultMemberBinding
{
Member = x,
MemberType = x.PropertyType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
});
var fields = resultType.GetFields(BindingFlags.Instance | BindingFlags.Public)
.Where(x => !x.IsInitOnly && !x.IsLiteral)
.Select(x => new ResultMemberBinding
{
Member = x,
MemberType = x.FieldType,
SortOrder = x.MetadataToken,
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
});
return properties.Concat(fields)
.Where(x => x.Attribute != null)
.Where(x => IsMainResultBinding(x.Attribute))
.OrderBy(x => x.SortOrder)
.ToList();
}
private static void BindResultMembers(object instance, IDataReader reader, IList<ResultMemberBinding> bindings)
{
ValidateBindings(instance.GetType(), bindings);
int currentIndex = 0;
bool hasCurrent = true;
for (int i = 0; i < bindings.Count; i++)
{
ResultMemberBinding binding = bindings[i];
while (hasCurrent && currentIndex < binding.Attribute.ResultIndex)
{
hasCurrent = reader.NextResult();
currentIndex++;
}
if (!hasCurrent || currentIndex != binding.Attribute.ResultIndex)
break;
object value = ReadResultValue(binding.MemberType, binding.Attribute, reader);
binding.SetValue(instance, value);
if (i + 1 < bindings.Count)
{
hasCurrent = reader.NextResult();
currentIndex++;
}
}
}
private static void ValidateBindings(Type resultType, IList<ResultMemberBinding> 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 void BindMainResultMembers(object instance, object mainResult)
{
if (instance == null)
return;
IList<ResultMemberBinding> bindings = GetMainResultBindings(instance.GetType());
if (bindings.Count == 0)
return;
if (bindings.Count > 1)
throw new InvalidOperationException(string.Format("Type '{0}' defines multiple ProcedureResultAttribute bindings for the main procedure result.", instance.GetType().FullName));
ResultMemberBinding binding = bindings[0];
object value = ConvertScalarValue(binding.MemberType, mainResult == DBNull.Value ? null : mainResult);
binding.SetValue(instance, value);
}
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();
return ConvertScalarValue(propertyType, value);
}
private static object ReadSimpleList(Type propertyType, Type elementType, ProcedureResultAttribute attr, IDataReader reader)
{
Type targetElementType = Nullable.GetUnderlyingType(elementType) ?? elementType;
Type listType = typeof(List<>).MakeGenericType(elementType);
var list = (System.Collections.IList)Activator.CreateInstance(listType);
int ordinal = -1;
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 (targetElementType == typeof(Guid))
{
Guid g;
if (Guid.TryParse(value.ToString(), out g))
list.Add(elementType == typeof(Guid) ? (object)g : new Guid?(g));
else
list.Add(elementType.GetDefaultValue());
}
else if (targetElementType.IsEnum)
list.Add(Enum.ToObject(targetElementType, value));
else
list.Add(targetElementType == elementType ? elementType.CastObject(value) : targetElementType.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;
return reader.GetOrdinal(attr.ColumnName);
}
private static bool IsMainResultBinding(ProcedureResultAttribute attribute)
{
return attribute != null && attribute.ResultIndex < 0;
}
private static object ConvertScalarValue(Type targetType, object value)
{
if (value == null)
return targetType.GetDefaultValue();
Type underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (underlyingType == typeof(Guid))
{
Guid g;
if (Guid.TryParse(value.ToString(), out g))
return targetType == typeof(Guid) ? (object)g : new Guid?(g);
return targetType.GetDefaultValue();
}
if (underlyingType.IsEnum)
return Enum.ToObject(underlyingType, value);
return underlyingType == targetType ? targetType.CastObject(value) : underlyingType.CastObject(value);
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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
{
/// <summary>Allows to add stored procedure metadata to class.</summary>
[AttributeUsage(AttributeTargets.Class)]
public class ProcedureAttribute : Attribute
{
/// <summary>Gets or sets procedure owner name.</summary>
public string Owner { get; set; }
/// <summary>Gets or sets procedure name.</summary>
public string Name { get; set; }
/// <summary>Gets or sets a value indicating whether metadata overrides other defaults.</summary>
public bool Override { get; set; }
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.Data;
namespace DynamORM.Mapper
{
/// <summary>Declares metadata for object-based stored procedure parameters.</summary>
[AttributeUsage(AttributeTargets.Property)]
public class ProcedureParameterAttribute : ColumnAttribute
{
/// <summary>Sentinel used when database type was not provided.</summary>
public const int UnspecifiedDbType = -1;
/// <summary>Sentinel used when size was not provided.</summary>
public const int UnspecifiedSize = -1;
/// <summary>Sentinel used when precision or scale was not provided.</summary>
public const byte UnspecifiedByte = byte.MaxValue;
/// <summary>Gets or sets parameter direction. Defaults to input.</summary>
public ParameterDirection Direction { get; set; }
/// <summary>Gets or sets explicit parameter order. Lower values are emitted first.</summary>
public int Order { get; set; }
/// <summary>Gets or sets parameter database type.</summary>
public DbType DbType { get; set; }
/// <summary>Gets or sets parameter size.</summary>
public new int Size { get; set; }
/// <summary>Gets or sets parameter precision.</summary>
public new byte Precision { get; set; }
/// <summary>Gets or sets parameter scale.</summary>
public new byte Scale { get; set; }
/// <summary>Initializes a new instance of the <see cref="ProcedureParameterAttribute"/> class.</summary>
public ProcedureParameterAttribute()
{
Direction = ParameterDirection.Input;
Order = int.MaxValue;
DbType = (DbType)UnspecifiedDbType;
Size = UnspecifiedSize;
Precision = UnspecifiedByte;
Scale = UnspecifiedByte;
}
/// <summary>Initializes a new instance of the <see cref="ProcedureParameterAttribute"/> class.</summary>
public ProcedureParameterAttribute(string name)
: this()
{
Name = name;
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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
{
/// <summary>Declares mapping of a typed procedure result property to a specific result set.</summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class ProcedureResultAttribute : Attribute
{
/// <summary>Main procedure result marker.</summary>
public const int MainResultIndex = -1;
/// <summary>Initializes a new instance of the <see cref="ProcedureResultAttribute"/> class.</summary>
public ProcedureResultAttribute()
: this(MainResultIndex)
{
}
/// <summary>Initializes a new instance of the <see cref="ProcedureResultAttribute"/> class.</summary>
public ProcedureResultAttribute(int resultIndex)
{
ResultIndex = resultIndex;
}
/// <summary>Gets result-set index in reader order, zero based.</summary>
public int ResultIndex { get; private set; }
/// <summary>Gets or sets optional column name for scalar/simple list extraction.</summary>
public string ColumnName { get; set; }
/// <summary>Gets or sets optional name used for DataTable.</summary>
public string Name { get; set; }
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.
*/
namespace DynamORM.Objects
{
/// <summary>Exposes typed stored procedure descriptor metadata.</summary>
public interface IProcedureDescriptor
{
}
/// <summary>Exposes typed stored procedure descriptor metadata with explicit result type.</summary>
/// <typeparam name="TResult">Procedure result type.</typeparam>
public interface IProcedureDescriptor<TResult> : IProcedureDescriptor
{
}
/// <summary>Base class for typed stored procedure descriptors.</summary>
/// <typeparam name="TArgs">Procedure arguments contract.</typeparam>
public abstract class Procedure<TArgs>
: IProcedureDescriptor
where TArgs : IProcedureParameters
{
}
/// <summary>Base class for typed stored procedure descriptors with explicit result model.</summary>
/// <typeparam name="TArgs">Procedure arguments contract.</typeparam>
/// <typeparam name="TResult">Procedure result model.</typeparam>
public abstract class Procedure<TArgs, TResult> : Procedure<TArgs>, IProcedureDescriptor<TResult>
where TArgs : IProcedureParameters
{
}
}

View File

@@ -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.Objects
{
/// <summary>Marks an object as an explicit stored procedure parameter contract.</summary>
public interface IProcedureParameters
{
}
/// <summary>Marks an object as a stored procedure parameter contract with a declared typed result model.</summary>
/// <typeparam name="TResult">Typed result model.</typeparam>
public interface IProcedureParameters<TResult> : IProcedureParameters
{
}
/// <summary>Allows typed procedure result models to consume multiple result sets directly.</summary>
public interface IProcedureResultReader
{
/// <summary>Reads all required result sets from the procedure reader.</summary>
/// <param name="reader">Procedure result reader, usually a cached reader.</param>
void ReadResults(IDataReader reader);
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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 DynamORM.Objects;
namespace DynamORM
{
/// <summary>Typed stored procedure execution handle.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
public class TypedProcedureCall<TProcedure>
where TProcedure : IProcedureDescriptor
{
protected readonly DynamicProcedureInvoker Invoker;
internal TypedProcedureCall(DynamicProcedureInvoker invoker)
{
Invoker = invoker;
}
/// <summary>Execute stored procedure descriptor.</summary>
/// <param name="args">Optional procedure arguments contract.</param>
/// <returns>Procedure result.</returns>
public virtual object Exec(object args = null)
{
return Invoker.Exec<TProcedure>(args);
}
}
/// <summary>Typed stored procedure execution handle with strong result type.</summary>
/// <typeparam name="TProcedure">Procedure descriptor type.</typeparam>
/// <typeparam name="TResult">Procedure result type.</typeparam>
public class TypedProcedureCall<TProcedure, TResult> : TypedProcedureCall<TProcedure>
where TProcedure : IProcedureDescriptor<TResult>
{
internal TypedProcedureCall(DynamicProcedureInvoker invoker)
: base(invoker)
{
}
/// <summary>Execute stored procedure descriptor.</summary>
/// <param name="args">Optional procedure arguments contract.</param>
/// <returns>Procedure result.</returns>
public new virtual TResult Exec(object args = null)
{
return Invoker.Exec<TProcedure, TResult>(args);
}
}
}

View File

@@ -79,3 +79,15 @@ table.Delete(code: "201");
```
These forms are validated in `DynamORM.Tests/Modify/DynamicModificationTests.cs`.
## Stored Procedures
Three common entry points:
```csharp
var legacy = db.Procedures.sp_DoWork(id: 10, ret_code: 0);
var typed = db.Procedure<MyProcedure>(new MyProcedureArgs());
var typedResult = db.TypedProcedure<MyProcedure, MyProcedureResult>().Exec(new MyProcedureArgs());
```
Full details are documented in [Stored Procedures](stored-procedures.md).

View File

@@ -1,30 +1,40 @@
# Stored Procedures
Stored procedure support is available through `db.Procedures` when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
Stored procedure support is available when `DynamicDatabaseOptions.SupportStoredProcedures` is enabled.
This page documents actual runtime behavior from `DynamicProcedureInvoker`.
There are now five practical ways to call procedures:
1. old dynamic invocation through `db.Procedures.SomeProc(...)`
2. parameter-contract object invocation through `db.Procedures.SomeProc(new Args())`
3. typed descriptor invocation through `db.Procedures.Exec<TProcedure>(args)` or `db.Procedure<TProcedure>(args)`
4. strongly typed direct calls through `db.Procedures.Exec<TProcedure, TResult>(args)` or `db.Procedure<TProcedure, TResult>(args)`
5. typed handle calls through `db.Procedures.Typed<TProcedure, TResult>().Exec(args)` or `db.TypedProcedure<TProcedure, TResult>().Exec(args)`
This page documents current runtime behavior from `DynamicProcedureInvoker`, `DynamicProcedureParameterBinder`, `DynamicProcedureResultBinder`, and the typed procedure descriptor APIs.
## `SupportStoredProceduresResult` and Provider Differences
`DynamicProcedureInvoker` can treat the procedure "main result" as either:
- affected rows from `ExecuteNonQuery()`
- provider return value parameter
- provider return-value parameter
This behavior is controlled by `DynamicDatabaseOptions.SupportStoredProceduresResult`.
- `true`: if a return-value parameter is present, invoker uses that value as main result
- `false`: invoker keeps `ExecuteNonQuery()` result (safer for providers that do not expose SQL Server-like return value behavior)
- `false`: invoker keeps `ExecuteNonQuery()` result
Why this matters:
Why this exists:
- SQL Server commonly supports procedure return values in this style
- some providers (for example Oracle setups) do not behave the same way
- forcing return-value extraction on such providers can cause runtime errors or invalid result handling
- some providers, including Oracle-style setups, do not behave the same way
- forcing SQL Server-like return-value handling can cause runtime errors or invalid result handling on those providers
If procedures fail on non-SQL Server providers, first disable `SupportStoredProceduresResult` and rely on explicit `out_` parameters for status/result codes.
If procedures fail on non-SQL Server providers, first disable `SupportStoredProceduresResult` and rely on explicit output parameters for status/result codes.
## Invocation Basics
## 1. Old Dynamic Invocation
This is the original API:
```csharp
var scalar = db.Procedures.sp_Exp_Scalar();
@@ -41,7 +51,7 @@ Final command name is `dbo.reporting.MyProc`.
## Parameter Direction Prefixes
Because dynamic `out/ref` is limited, parameter direction is encoded by argument name prefix:
Because C# dynamic invocation cannot use normal `out`/`ref` in this surface, parameter direction is encoded by argument name prefix:
- `out_` => `ParameterDirection.Output`
- `ret_` => `ParameterDirection.ReturnValue`
@@ -57,15 +67,15 @@ dynamic res = db.Procedures.sp_Message_SetState(
ret_code: 0);
```
Prefix is removed from the exposed output key (`out_message` -> `message`).
Prefix is removed from the exposed output key, so `out_message` becomes `message` in the returned payload.
## How to Specify Type/Length for Out Parameters
## How to Specify Type, Length, Precision, or Scale for Output Parameters
This is the most common pain point. You have 2 primary options.
This is the most common issue with the old dynamic API.
## Option A: `DynamicSchemaColumn` (recommended for output-only)
### Option A: `DynamicSchemaColumn`
Use this when you need explicit type, length, precision, or scale.
Use this for output-only parameters when schema must be explicit.
```csharp
dynamic res = db.Procedures.sp_Message_SetState(
@@ -79,9 +89,9 @@ dynamic res = db.Procedures.sp_Message_SetState(
ret_code: 0);
```
## Option B: `DynamicColumn` (recommended for input/output with value)
### Option B: `DynamicColumn`
Use this when you need direction + value + schema in one object.
Use this when the parameter is input/output and you need both value and schema.
```csharp
dynamic res = db.Procedures.sp_Message_SetState(
@@ -99,9 +109,9 @@ dynamic res = db.Procedures.sp_Message_SetState(
});
```
## Option C: Plain value + name prefix
### Option C: plain value with prefixed argument name
Quickest form, but type/size inference is automatic.
Fastest form, but type/size inference is automatic.
```csharp
dynamic res = db.Procedures.sp_Message_SetState(
@@ -110,62 +120,66 @@ dynamic res = db.Procedures.sp_Message_SetState(
ret_code: 0);
```
Use Option A/B whenever output size/type must be controlled.
Use `DynamicSchemaColumn` or `DynamicColumn` whenever output schema must be controlled.
## Return Shape Matrix (What You Actually Get)
## Result Shape Matrix for Old Dynamic Invocation
`DynamicProcedureInvoker` chooses result shape from generic type arguments.
### No generic type arguments
## No generic type arguments (`db.Procedures.MyProc(...)`)
`db.Procedures.MyProc(...)`
Main result path:
- procedure executes via `ExecuteNonQuery()`
- if return-value support is enabled and return param is present, that value replaces affected-row count
- if return-value support is enabled and a return-value parameter is present, that value replaces affected-row count
Final returned object:
- if no out/ret params were requested: scalar main result (`int` or return value)
- if no out/ret/both params were requested: scalar main result
- if any out/ret/both params were requested: dynamic object with keys
## One generic type argument (`MyProc<TMain>(...)`)
### One generic type argument
Main result type resolution:
`db.Procedures.MyProc<TMain>(...)`
- `TMain == IDataReader` => returns cached reader (`DynamicCachedReader`)
- `TMain == DataTable` => returns `DataTable`
- `TMain == List<object>` or `IEnumerable<object>` => list of dynamic rows
- `TMain == List<primitive/string>` => converted first-column list
- `TMain == List<complex>` => mapped list
- `TMain == primitive/string` => scalar converted value
- `TMain == complex class` => first row mapped to class (or `null`)
Main result resolution:
- `TMain == IDataReader` => cached reader (`DynamicCachedReader`)
- `TMain == DataTable` => `DataTable`
- `TMain == IEnumerable<object>` / `List<object>` => list of dynamic rows
- `TMain == IEnumerable<primitive>` / `List<primitive>` => first-column converted list
- `TMain == IEnumerable<complex>` / `List<complex>` => mapped list
- `TMain == primitive/string/Guid` => converted scalar
- `TMain == complex class` => first row mapped to class or `null`
Final returned object:
- if no out/ret params were requested: `TMain` result directly
- if out/ret params exist: dynamic object containing main result + out params
- if no out/ret params were requested: `TMain`
- if out/ret params exist: dynamic payload containing main result plus out params
Important nuance:
- when out params exist and only one generic type is provided, the result is a dynamic object, not bare `TMain`.
- with out params and only one generic type, the final result is payload-shaped, not raw `TMain`
## Two generic type arguments (`MyProc<TMain, TOutModel>(...)`)
### Two generic type arguments
This is the preferred pattern when out params are involved.
`db.Procedures.MyProc<TMain, TOutModel>(...)`
This is the original preferred pattern when out params are involved.
- `TMain` is resolved as above
- out/main values are packed into a dictionary-like dynamic payload
- if mapper for `TOutModel` exists, payload is mapped to `TOutModel`
- otherwise fallback is dynamic object
- main result and out params are packed into one payload
- if `TOutModel` is mappable, payload is mapped to `TOutModel`
- otherwise the result falls back to dynamic payload
## Result Key Names in Out Payload
### Out payload key names
When out payload is used:
- main result is stored under procedure method name key (`binder.Name`)
- each out/both/ret param is stored under normalized parameter name (without prefix)
- main result is stored under procedure method name key
- each out/both/ret param is stored under normalized parameter name
Example call:
Example:
```csharp
var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
@@ -176,89 +190,314 @@ var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
Expected payload keys before mapping:
- `sp_Message_SetState` (main result)
- `sp_Message_SetState`
- `message`
- `code`
## Preparing Result Classes Correctly
## 2. Parameter Contract Object Invocation
Use `ColumnAttribute` to map returned payload keys.
This is the new object-contract path.
Instead of encoding output metadata into prefixed dynamic arguments, you pass a single object implementing `IProcedureParameters`.
```csharp
public class MessageSetStateResult
public class SetMessageStatusArgs : IProcedureParameters<SetMessageStatusResult>
{
[Column("sp_Message_SetState")]
public int MainResult { get; set; }
[ProcedureParameter("status", Direction = ParameterDirection.ReturnValue, Order = 1)]
public int Status { get; set; }
[Column("message")]
public string Message { get; set; }
[ProcedureParameter("id", Order = 2, DbType = DbType.String, Size = 64)]
public string Id { get; set; }
[Column("code")]
public int ReturnCode { get; set; }
[ProcedureParameter("result", Direction = ParameterDirection.Output, Order = 3, DbType = DbType.Int32)]
public int Result { get; set; }
[ProcedureParameter("description", Direction = ParameterDirection.InputOutput, Order = 4, DbType = DbType.String, Size = 1024)]
public string Description { get; set; }
}
```
If key names and property names already match, attributes are optional.
## Common Patterns
## Pattern 1: Main scalar only
Then:
```csharp
int count = db.Procedures.sp_CountMessages<int>();
var res = db.Procedures.sp_SetMessageStatus(new SetMessageStatusArgs
{
Id = "A-100",
Description = "seed"
});
```
## Pattern 2: Output params + mapped output model
Rules:
- the object must implement `IProcedureParameters`
- object mode activates only when exactly one argument is passed
- `ProcedureParameterAttribute` controls name, direction, order, type, size, precision, and scale
- `ColumnAttribute` can still provide fallback name/type/size metadata for compatibility
## Why use parameter contracts
Advantages:
- one argument object instead of prefixed parameter names
- output and return-value schema are explicit on the contract
- order is stable and documented in one place
- result type can be declared through `IProcedureParameters<TResult>`
## 3. Typed Procedure Descriptors
You can now describe a procedure with a class-level descriptor.
```csharp
var res = db.Procedures.sp_Message_SetState<int, MessageSetStateResult>(
id: "abc-001",
status: 2,
out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 },
ret_code: 0);
[Procedure(Name = "sp_set_message_status", Owner = "dbo")]
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs>
{
}
```
## Pattern 3: Procedure returning dataset as table
This enables:
```csharp
DataTable dt = db.Procedures.sp_Message_GetBatch<DataTable>(batchId: 10);
var res1 = db.Procedures.Exec<SetMessageStatusProcedure>(args);
var res2 = db.Procedure<SetMessageStatusProcedure>(args);
```
## Pattern 4: Procedure returning mapped collection
Descriptor rules:
- `ProcedureAttribute` is similar to `TableAttribute`
- `Name` controls procedure name
- `Owner` adds schema/owner prefix
- if `ProcedureAttribute` is omitted, descriptor class name is used as procedure name
- descriptor must inherit from `Procedure<TArgs>` or `Procedure<TArgs, TResult>`
- `TArgs` must implement `IProcedureParameters`
## 4. Strongly Typed Direct Calls
If the descriptor declares a result type, you can use the explicit strongly typed overloads.
Descriptor:
```csharp
List<MessageRow> rows = db.Procedures.sp_Message_List<List<MessageRow>>(status: 1);
[Procedure(Name = "sp_set_message_status")]
public class SetMessageStatusProcedure : Procedure<SetMessageStatusArgs, SetMessageStatusResult>
{
}
```
## Pattern 5: Read output dynamically
Calls:
```csharp
dynamic res = db.Procedures.sp_Message_SetState(
id: "abc-001",
out_message: new DynamicSchemaColumn { Name = "message", Type = DbType.String, Size = 1024 },
ret_code: 0);
var message = (string)res.message;
var code = (int)res.code;
var res1 = db.Procedures.Exec<SetMessageStatusProcedure, SetMessageStatusResult>(args);
var res2 = db.Procedure<SetMessageStatusProcedure, SetMessageStatusResult>(args);
```
Why this needs two generic arguments:
- C# cannot infer a method return type from `TProcedure : IProcedureDescriptor<TResult>` alone
- the descriptor constraint is enough for validation, but not enough for single-generic return typing
So the two-generic overload is the direct strongly typed API.
## 5. Typed Handle Calls
To avoid repeating the result generic on the `Exec(...)` step, you can create a typed execution handle.
```csharp
var res1 = db.Procedures.Typed<SetMessageStatusProcedure, SetMessageStatusResult>().Exec(args);
var res2 = db.TypedProcedure<SetMessageStatusProcedure, SetMessageStatusResult>().Exec(args);
```
There is also a non-result variant:
```csharp
var res = db.TypedProcedure<SetMessageStatusProcedure>().Exec(args);
```
This API exists because it gives one setup step with descriptor/result typing, then a normal strongly typed `Exec(...)` call.
## Declaring Typed Procedure Results
There are three supported result declaration patterns.
### Pattern A: payload mapping with `ColumnAttribute`
This is the classic approach.
```csharp
public class SetMessageStatusResult
{
[Column("sp_set_message_status")]
public int MainResult { get; set; }
[Column("result")]
public int Result { get; set; }
[Column("description")]
public string Description { get; set; }
[Column("status")]
public int Status { get; set; }
}
```
### Pattern B: main result through `ProcedureResultAttribute`
This is now also supported.
```csharp
public class SetMessageStatusResult
{
[ProcedureResult]
public int MainResult { get; set; }
[Column("result")]
public int Result { get; set; }
[Column("description")]
public string Description { get; set; }
}
```
Equivalent explicit form:
```csharp
[ProcedureResult(-1)]
public int MainResult { get; set; }
```
Notes:
- `[Column("ProcedureName")]` is still supported and unchanged
- `[ProcedureResult]` / `[ProcedureResult(-1)]` is additive, not a replacement
- only one main-result member is allowed on a result type
### Pattern C: multiple reader result sets through `ProcedureResultAttribute`
A typed result can bind individual reader result sets to specific members.
```csharp
public class BatchResult
{
[ProcedureResult]
public int MainResult { get; set; }
[Column("status")]
public int Status { get; set; }
[ProcedureResult(0, ColumnName = "Code")]
public string FirstCode { get; set; }
[ProcedureResult(1, ColumnName = "Code")]
public List<string> Codes { get; set; }
[ProcedureResult(2, ColumnName = "State")]
public int[] States { get; set; }
[ProcedureResult(3)]
public UserRow User { get; set; }
[ProcedureResult(4)]
public List<UserRow> Users { get; set; }
[ProcedureResult(5, Name = "codes_table")]
public DataTable CodesTable { get; set; }
}
```
Supported member shapes:
- scalar/simple value
- list or array of simple values
- mapped complex object
- list or array of mapped complex objects
- `DataTable`
- public property or public field
For scalar and simple-list members:
- first column is used by default
- or `ColumnName` can select a specific column
For `DataTable` members:
- `Name` becomes the table name
## Custom Multi-Result Reader Logic
If the declarative `ProcedureResultAttribute` model is not enough, the result type can implement `IProcedureResultReader`.
```csharp
public class BatchResult : IProcedureResultReader
{
public List<string> Codes { get; } = new List<string>();
public List<int> States { get; } = new List<int>();
public void ReadResults(IDataReader reader)
{
while (reader.Read())
Codes.Add(reader.GetString(0));
if (reader.NextResult())
while (reader.Read())
States.Add(reader.GetInt32(0));
}
}
```
This remains the escape hatch for complex provider-specific or custom reading logic.
## Which Result Type Wins
Result type precedence for the new APIs:
1. explicit descriptor result from `Procedure<TArgs, TResult>`
2. declared result from `IProcedureParameters<TResult>` on the argument contract
3. old generic result arguments from dynamic invocation
4. fallback dynamic payload/scalar behavior
That allows descriptor-level result typing to override argument-level declarations when needed.
## Recommended Usage Patterns
### Keep using old dynamic invocation when
- the procedure is simple
- you want minimal ceremony
- outputs are few and dynamic payload is acceptable
### Use parameter contracts when
- output metadata is important
- procedures have many parameters
- you want stable parameter ordering and schema definition
### Use typed descriptors when
- the procedure is reused across the codebase
- you want one named procedure contract per stored procedure
- you want static procedure-name metadata instead of string-based calls
### Use strongly typed direct calls when
- you want a single call expression returning `TResult`
- you are fine with `Exec<TProcedure, TResult>(...)` or `Procedure<TProcedure, TResult>(...)`
### Use typed handles when
- you want the strongest typing without relying on dynamic dispatch
- you want to configure the descriptor/result type once and then call `.Exec(...)`
## Troubleshooting Checklist
- Out value is truncated or null:
- define output schema explicitly with `DynamicSchemaColumn` (type + size)
- Unexpected return object shape:
- check whether any out/ret/both parameter was passed
- if yes, expect out payload object unless you used 2-generic mapping variant
- Mapping to class fails silently (dynamic fallback):
- ensure output model is mappable and keys match columns/properties
- Return value not appearing:
- ensure `ret_` parameter is supplied
- ensure provider option `SupportStoredProceduresResult` matches your DB behavior
- Procedure call errors on non-SQL Server providers:
- set `SupportStoredProceduresResult = false`
- return status via explicit `out_` parameters instead of return-value semantics
## Notes
- Behavior is implemented in `DynamORM/DynamicProcedureInvoker.cs`.
- XML examples also appear in `DynamORM/DynamicDatabase.cs`.
- output value is truncated or null:
- define explicit schema with `ProcedureParameterAttribute`, `DynamicSchemaColumn`, or `DynamicColumn`
- unexpected return object shape:
- check whether out/ret/inputoutput parameters are present
- old dynamic invocation changes shape when out params are present
- typed descriptor rejects arguments:
- verify the argument object type matches the descriptor's `TArgs`
- mapped result is incomplete:
- verify payload keys match `ColumnAttribute` names
- verify `ProcedureResultAttribute` indexes match actual reader result-set order
- procedure fails on non-SQL Server provider:
- disable `SupportStoredProceduresResult`
- return status through explicit output parameters instead of SQL Server-style return-value semantics