Merge feature/procedure-parameter-object
This commit is contained in:
File diff suppressed because it is too large
Load Diff
135
DynamORM.Tests/Helpers/FakeDbCommand.cs
Normal file
135
DynamORM.Tests/Helpers/FakeDbCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
135
DynamORM.Tests/Helpers/FakeMultiResultDataReader.cs
Normal file
135
DynamORM.Tests/Helpers/FakeMultiResultDataReader.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
156
DynamORM.Tests/Helpers/ProcedureParameterModels.cs
Normal file
156
DynamORM.Tests/Helpers/ProcedureParameterModels.cs
Normal 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>
|
||||
{
|
||||
}
|
||||
}
|
||||
383
DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs
Normal file
383
DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
100
DynamORM/Helpers/DynamicProcedureDescriptor.cs
Normal file
100
DynamORM/Helpers/DynamicProcedureDescriptor.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
170
DynamORM/Helpers/DynamicProcedureParameterBinder.cs
Normal file
170
DynamORM/Helpers/DynamicProcedureParameterBinder.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
452
DynamORM/Helpers/DynamicProcedureResultBinder.cs
Normal file
452
DynamORM/Helpers/DynamicProcedureResultBinder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
DynamORM/Mapper/ProcedureAttribute.cs
Normal file
46
DynamORM/Mapper/ProcedureAttribute.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
83
DynamORM/Mapper/ProcedureParameterAttribute.cs
Normal file
83
DynamORM/Mapper/ProcedureParameterAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
61
DynamORM/Mapper/ProcedureResultAttribute.cs
Normal file
61
DynamORM/Mapper/ProcedureResultAttribute.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
57
DynamORM/Objects/Procedure.cs
Normal file
57
DynamORM/Objects/Procedure.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
51
DynamORM/Objects/ProcedureContracts.cs
Normal file
51
DynamORM/Objects/ProcedureContracts.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
73
DynamORM/TypedProcedureCall.cs
Normal file
73
DynamORM/TypedProcedureCall.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user