diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs
index 7fb9fe2..55e0de4 100644
--- a/AmalgamationTool/DynamORM.Amalgamation.cs
+++ b/AmalgamationTool/DynamORM.Amalgamation.cs
@@ -10,6 +10,7 @@ using DynamORM.Builders;
using DynamORM.Helpers.Dynamics;
using DynamORM.Helpers;
using DynamORM.Mapper;
+using DynamORM.Objects;
using DynamORM.TypedSql;
using DynamORM.Validation;
using System.Collections.Concurrent;
@@ -2680,6 +2681,69 @@ namespace DynamORM
.ExecuteNonQuery();
}
}
+ /// Execute typed stored procedure descriptor.
+ /// Procedure descriptor type.
+ /// Procedure result.
+ public virtual object Procedure()
+ {
+ return Procedure(null);
+ }
+ /// Execute typed stored procedure descriptor.
+ /// Procedure descriptor type.
+ /// Procedure arguments contract.
+ /// Procedure result.
+ public virtual object Procedure(object args)
+ {
+ if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
+ throw new InvalidOperationException("Database connection desn't support stored procedures.");
+
+ return new DynamicProcedureInvoker(this).Exec(args);
+ }
+ /// Execute typed stored procedure descriptor with strong result type.
+ /// Procedure descriptor type.
+ /// Procedure result type.
+ /// Procedure result.
+ public virtual TResult Procedure()
+ where TProcedure : IProcedureDescriptor
+ {
+ return Procedure(null);
+ }
+ /// Execute typed stored procedure descriptor with strong result type.
+ /// Procedure descriptor type.
+ /// Procedure result type.
+ /// Procedure arguments contract.
+ /// Procedure result.
+ public virtual TResult Procedure(object args)
+ where TProcedure : IProcedureDescriptor
+ {
+ if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
+ throw new InvalidOperationException("Database connection desn't support stored procedures.");
+
+ return new DynamicProcedureInvoker(this).Exec(args);
+ }
+ /// Create typed stored procedure execution handle.
+ /// Procedure descriptor type.
+ /// Typed execution handle.
+ public virtual TypedProcedureCall TypedProcedure()
+ where TProcedure : IProcedureDescriptor
+ {
+ if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
+ throw new InvalidOperationException("Database connection desn't support stored procedures.");
+
+ return new TypedProcedureCall(new DynamicProcedureInvoker(this));
+ }
+ /// Create typed stored procedure execution handle.
+ /// Procedure descriptor type.
+ /// Procedure result type.
+ /// Typed execution handle.
+ public virtual TypedProcedureCall TypedProcedure()
+ where TProcedure : IProcedureDescriptor
+ {
+ if ((Options & DynamicDatabaseOptions.SupportStoredProcedures) != DynamicDatabaseOptions.SupportStoredProcedures)
+ throw new InvalidOperationException("Database connection desn't support stored procedures.");
+
+ return new TypedProcedureCall(new DynamicProcedureInvoker(this));
+ }
#endregion Procedure
#region Execute
@@ -5330,6 +5394,50 @@ namespace DynamORM
_prefixes = prefixes;
_db = db;
}
+ /// Execute typed stored procedure descriptor.
+ /// Procedure descriptor type.
+ /// Optional procedure arguments contract.
+ /// Procedure result.
+ public virtual object Exec(object args = null)
+ {
+ DynamicProcedureDescriptor descriptor = DynamicProcedureDescriptor.Resolve(typeof(TProcedure));
+ return InvokeProcedure(
+ descriptor.ProcedureName,
+ descriptor.ResultName,
+ new List(),
+ args == null ? new object[0] : new[] { args },
+ new CallInfo(args == null ? 0 : 1),
+ descriptor.ResultType,
+ descriptor.ArgumentsType,
+ descriptor.ProcedureType);
+ }
+ /// Execute typed stored procedure descriptor with strong result type.
+ /// Procedure descriptor type.
+ /// Procedure result type.
+ /// Optional procedure arguments contract.
+ /// Procedure result.
+ public virtual TResult Exec(object args = null)
+ where TProcedure : IProcedureDescriptor
+ {
+ return ConvertProcedureResult(Exec(args));
+ }
+ /// Create typed stored procedure execution handle.
+ /// Procedure descriptor type.
+ /// Typed execution handle.
+ public virtual TypedProcedureCall Typed()
+ where TProcedure : IProcedureDescriptor
+ {
+ return new TypedProcedureCall(this);
+ }
+ /// Create typed stored procedure execution handle.
+ /// Procedure descriptor type.
+ /// Procedure result type.
+ /// Typed execution handle.
+ public virtual TypedProcedureCall Typed()
+ where TProcedure : IProcedureDescriptor
+ {
+ return new TypedProcedureCall(this);
+ }
/// This is where the magic begins.
/// Binder to create owner.
/// Binder invoke result.
@@ -5358,152 +5466,145 @@ namespace DynamORM
CallInfo info = binder.CallInfo;
// Get generic types
- IList types = binder.GetGenericTypeArguments();
+ IList types = binder.GetGenericTypeArguments() ?? new List();
+ result = InvokeProcedure(binder.Name, binder.Name, types, args, info, null, null, null);
+ return true;
+ }
+ internal object InvokeProcedure(string procedureName, string resultName, IList types, object[] args, CallInfo info, Type declaredResultType, Type expectedArgumentsType, Type procedureType)
+ {
+ object result;
Dictionary retParams = null;
+ if (expectedArgumentsType != null)
+ {
+ if (args.Length > 1)
+ throw new InvalidOperationException("Exec(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, binder.Name);
+ cmd.SetCommand(CommandType.StoredProcedure, procedureName);
else
- cmd.SetCommand(CommandType.StoredProcedure, string.Format("{0}.{1}", string.Join(".", _prefixes), binder.Name));
+ cmd.SetCommand(CommandType.StoredProcedure, string.Format("{0}.{1}", string.Join(".", _prefixes), procedureName));
#region Prepare arguments
int alen = args.Length;
bool retIsAdded = false;
+ if (declaredResultType == null)
+ declaredResultType = alen == 1 ? DynamicProcedureResultBinder.GetDeclaredResultType(args[0]) : null;
if (alen > 0)
{
- for (int i = 0; i < alen; i++)
+ if (alen == 1 && DynamicProcedureParameterBinder.CanBind(args[0]))
{
- 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)
+ DynamicProcedureParameterBinder.BindingResult bindingResult = DynamicProcedureParameterBinder.Bind(_db, cmd, args[0]);
+ retParams = bindingResult.ReturnParameters;
+ retIsAdded = bindingResult.ReturnValueAdded;
+ }
+ else
+ {
+ for (int i = 0; i < alen; i++)
{
- var dcv = (DynamicColumn)arg;
+ object arg = args[i];
- string paramName = i.ToString();
- bool isOut = false;
- bool isRet = false;
- bool isBoth = false;
-
- if (info.ArgumentNames.Count > 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)
{
- isOut = info.ArgumentNames[i].StartsWith("out_");
- isRet = info.ArgumentNames[i].StartsWith("ret_");
- isBoth = info.ArgumentNames[i].StartsWith("both_");
+ var dcv = (DynamicColumn)arg;
- paramName = isOut || isRet ?
- info.ArgumentNames[i].Substring(4) :
- isBoth ? info.ArgumentNames[i].Substring(5) :
- info.ArgumentNames[i];
+ 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();
+ 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);
}
- paramName = dcv.Alias ?? dcv.ColumnName ??
- (dcv.Schema.HasValue ? dcv.Schema.Value.Name : null) ??
- paramName;
-
- if (!isOut && !isRet && !isBoth)
+ else if (arg is DynamicSchemaColumn)
{
- isOut = dcv.ParameterDirection == ParameterDirection.Output;
- isRet = dcv.ParameterDirection == ParameterDirection.ReturnValue;
- isBoth = dcv.ParameterDirection == ParameterDirection.InputOutput;
- }
- if (isRet)
- retIsAdded = true;
+ var dsc = (DynamicSchemaColumn)arg;
- if (isOut || isRet || isBoth)
- {
- if (retParams == null)
- retParams = new Dictionary();
- 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;
- 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_");
- 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();
- 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_");
+ 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;
- string paramName = isOut || isRet ?
- info.ArgumentNames[i].Substring(4) :
- isBoth ? info.ArgumentNames[i].Substring(5) :
- info.ArgumentNames[i];
-
- if (isOut || isBoth || isRet)
+ if (isOut || isRet || isBoth)
{
if (retParams == null)
retParams = new Dictionary();
@@ -5515,12 +5616,44 @@ namespace DynamORM
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);
+ dsc.Type, dsc.Size, dsc.Precision, dsc.Scale,
+ DBNull.Value);
}
else
- cmd.AddParameter(_db, arg);
+ {
+ 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();
+ 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);
+ }
}
}
}
@@ -5530,7 +5663,13 @@ namespace DynamORM
object mainResult = null;
- if (types.Count > 0)
+ if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType))
+ {
+ using (IDataReader rdr = cmd.ExecuteReader())
+ using (IDataReader cache = rdr.CachedReader())
+ mainResult = DynamicProcedureResultBinder.ReadDeclaredResult(declaredResultType, cache);
+ }
+ else if (types.Count > 0)
{
mainResult = types[0].GetDefaultValue();
@@ -5543,7 +5682,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())
{
@@ -5670,9 +5809,9 @@ namespace DynamORM
if (mainResult != null)
{
if (mainResult == DBNull.Value)
- res.Add(binder.Name, null);
+ res.Add(resultName, null);
else
- res.Add(binder.Name, mainResult);
+ res.Add(resultName, mainResult);
}
foreach (KeyValuePair pos in retParams)
res.Add(pos.Key, ((IDbDataParameter)cmd.Parameters[pos.Value]).Value);
@@ -5686,15 +5825,35 @@ namespace DynamORM
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 true;
+ return result;
+ }
+ private static TResult ConvertProcedureResult(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);
}
/// Performs application-defined tasks associated with
/// freeing, releasing, or resetting unmanaged resources.
@@ -6826,6 +6985,43 @@ namespace DynamORM
#endregion IExtendedDisposable Members
}
+ /// Typed stored procedure execution handle.
+ /// Procedure descriptor type.
+ public class TypedProcedureCall
+ where TProcedure : IProcedureDescriptor
+ {
+ protected readonly DynamicProcedureInvoker Invoker;
+
+ internal TypedProcedureCall(DynamicProcedureInvoker invoker)
+ {
+ Invoker = invoker;
+ }
+ /// Execute stored procedure descriptor.
+ /// Optional procedure arguments contract.
+ /// Procedure result.
+ public virtual object Exec(object args = null)
+ {
+ return Invoker.Exec(args);
+ }
+ }
+ /// Typed stored procedure execution handle with strong result type.
+ /// Procedure descriptor type.
+ /// Procedure result type.
+ public class TypedProcedureCall : TypedProcedureCall
+ where TProcedure : IProcedureDescriptor
+ {
+ internal TypedProcedureCall(DynamicProcedureInvoker invoker)
+ : base(invoker)
+ {
+ }
+ /// Execute stored procedure descriptor.
+ /// Optional procedure arguments contract.
+ /// Procedure result.
+ public new virtual TResult Exec(object args = null)
+ {
+ return Invoker.Exec(args);
+ }
+ }
namespace Builders
{
/// Typed join kind used by typed fluent builder APIs.
@@ -14416,6 +14612,567 @@ namespace DynamORM
return resultTable;
}
}
+ 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()
+ .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
+ };
+ }
+ }
+ internal static class DynamicProcedureParameterBinder
+ {
+ internal sealed class BindingResult
+ {
+ public bool ReturnValueAdded { get; set; }
+ public Dictionary 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().FirstOrDefault();
+ ColumnAttribute colAttr = property.GetCustomAttributes(typeof(ColumnAttribute), true).Cast().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();
+ 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 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().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;
+ }
+ }
+ 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 ? "" : resultType.FullName));
+
+ object instance = CreateDeclaredResult(resultType);
+
+ IList 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 returnValues, object existing = null)
+ {
+ if (resultType == null)
+ return existing ?? returnValues.ToDynamic();
+
+ Dictionary payload = new Dictionary();
+
+ if (mainResultName != null)
+ payload[mainResultName] = mainResult == DBNull.Value ? null : mainResult;
+
+ if (returnValues != null)
+ foreach (KeyValuePair item in returnValues)
+ payload[item.Key] = item.Value == DBNull.Value ? null : item.Value;
+
+ DynamicTypeMap mapper = DynamicMapperCache.GetMapper(resultType);
+ 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 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().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().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 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().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().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 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 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 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);
+ }
+ }
/// Framework detection and specific implementations.
public static class FrameworkTools
{
@@ -17411,6 +18168,93 @@ namespace DynamORM
public class IgnoreAttribute : Attribute
{
}
+ /// Allows to add stored procedure metadata to class.
+ [AttributeUsage(AttributeTargets.Class)]
+ public class ProcedureAttribute : Attribute
+ {
+ /// Gets or sets procedure owner name.
+ public string Owner { get; set; }
+
+ /// Gets or sets procedure name.
+ public string Name { get; set; }
+
+ /// Gets or sets a value indicating whether metadata overrides other defaults.
+ public bool Override { get; set; }
+ }
+ /// Declares metadata for object-based stored procedure parameters.
+ [AttributeUsage(AttributeTargets.Property)]
+ public class ProcedureParameterAttribute : ColumnAttribute
+ {
+ /// Sentinel used when database type was not provided.
+ public const int UnspecifiedDbType = -1;
+
+ /// Sentinel used when size was not provided.
+ public const int UnspecifiedSize = -1;
+
+ /// Sentinel used when precision or scale was not provided.
+ public const byte UnspecifiedByte = byte.MaxValue;
+
+ /// Gets or sets parameter direction. Defaults to input.
+ public ParameterDirection Direction { get; set; }
+
+ /// Gets or sets explicit parameter order. Lower values are emitted first.
+ public int Order { get; set; }
+
+ /// Gets or sets parameter database type.
+ public DbType DbType { get; set; }
+
+ /// Gets or sets parameter size.
+ public new int Size { get; set; }
+
+ /// Gets or sets parameter precision.
+ public new byte Precision { get; set; }
+
+ /// Gets or sets parameter scale.
+ public new byte Scale { get; set; }
+
+ /// Initializes a new instance of the class.
+ public ProcedureParameterAttribute()
+ {
+ Direction = ParameterDirection.Input;
+ Order = int.MaxValue;
+ DbType = (DbType)UnspecifiedDbType;
+ Size = UnspecifiedSize;
+ Precision = UnspecifiedByte;
+ Scale = UnspecifiedByte;
+ }
+ /// Initializes a new instance of the class.
+ public ProcedureParameterAttribute(string name)
+ : this()
+ {
+ Name = name;
+ }
+ }
+ /// Declares mapping of a typed procedure result property to a specific result set.
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+ public class ProcedureResultAttribute : Attribute
+ {
+ /// Main procedure result marker.
+ public const int MainResultIndex = -1;
+
+ /// Initializes a new instance of the class.
+ public ProcedureResultAttribute()
+ : this(MainResultIndex)
+ {
+ }
+ /// Initializes a new instance of the class.
+ public ProcedureResultAttribute(int resultIndex)
+ {
+ ResultIndex = resultIndex;
+ }
+ /// Gets result-set index in reader order, zero based.
+ public int ResultIndex { get; private set; }
+
+ /// Gets or sets optional column name for scalar/simple list extraction.
+ public string ColumnName { get; set; }
+
+ /// Gets or sets optional name used for DataTable.
+ public string Name { get; set; }
+ }
/// Allows to add table name to class.
[AttributeUsage(AttributeTargets.Class)]
public class TableAttribute : Attribute
@@ -17858,6 +18702,45 @@ namespace DynamORM
_database = null;
}
}
+ /// Exposes typed stored procedure descriptor metadata.
+ public interface IProcedureDescriptor
+ {
+ }
+ /// Exposes typed stored procedure descriptor metadata with explicit result type.
+ /// Procedure result type.
+ public interface IProcedureDescriptor : IProcedureDescriptor
+ {
+ }
+ /// Base class for typed stored procedure descriptors.
+ /// Procedure arguments contract.
+ public abstract class Procedure
+ : IProcedureDescriptor
+ where TArgs : IProcedureParameters
+ {
+ }
+ /// Base class for typed stored procedure descriptors with explicit result model.
+ /// Procedure arguments contract.
+ /// Procedure result model.
+ public abstract class Procedure : Procedure, IProcedureDescriptor
+ where TArgs : IProcedureParameters
+ {
+ }
+ /// Marks an object as an explicit stored procedure parameter contract.
+ public interface IProcedureParameters
+ {
+ }
+ /// Marks an object as a stored procedure parameter contract with a declared typed result model.
+ /// Typed result model.
+ public interface IProcedureParameters : IProcedureParameters
+ {
+ }
+ /// Allows typed procedure result models to consume multiple result sets directly.
+ public interface IProcedureResultReader
+ {
+ /// Reads all required result sets from the procedure reader.
+ /// Procedure result reader, usually a cached reader.
+ void ReadResults(IDataReader reader);
+ }
}
namespace TypedSql
{
diff --git a/DynamORM.Tests/Helpers/FakeDbCommand.cs b/DynamORM.Tests/Helpers/FakeDbCommand.cs
new file mode 100644
index 0000000..4e2f312
--- /dev/null
+++ b/DynamORM.Tests/Helpers/FakeDbCommand.cs
@@ -0,0 +1,135 @@
+/*
+ * DynamORM - Dynamic Object-Relational Mapping library.
+ * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com)
+ * All rights reserved.
+ */
+
+using System;
+using System.Collections;
+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