Add declarative procedure result-set binding
This commit is contained in:
@@ -10,6 +10,7 @@ using DynamORM.Builders;
|
|||||||
using DynamORM.Helpers.Dynamics;
|
using DynamORM.Helpers.Dynamics;
|
||||||
using DynamORM.Helpers;
|
using DynamORM.Helpers;
|
||||||
using DynamORM.Mapper;
|
using DynamORM.Mapper;
|
||||||
|
using DynamORM.Objects;
|
||||||
using DynamORM.TypedSql;
|
using DynamORM.TypedSql;
|
||||||
using DynamORM.Validation;
|
using DynamORM.Validation;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -5541,7 +5542,7 @@ namespace DynamORM
|
|||||||
|
|
||||||
object mainResult = null;
|
object mainResult = null;
|
||||||
|
|
||||||
if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType))
|
if (types.Count == 0 && DynamicProcedureResultBinder.HasDeclaredResultBinding(declaredResultType))
|
||||||
{
|
{
|
||||||
using (IDataReader rdr = cmd.ExecuteReader())
|
using (IDataReader rdr = cmd.ExecuteReader())
|
||||||
using (IDataReader cache = rdr.CachedReader())
|
using (IDataReader cache = rdr.CachedReader())
|
||||||
@@ -6849,70 +6850,6 @@ namespace DynamORM
|
|||||||
|
|
||||||
#endregion IExtendedDisposable Members
|
#endregion IExtendedDisposable Members
|
||||||
}
|
}
|
||||||
/// <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);
|
|
||||||
}
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
namespace Builders
|
namespace Builders
|
||||||
{
|
{
|
||||||
/// <summary>Typed join kind used by typed fluent builder APIs.</summary>
|
/// <summary>Typed join kind used by typed fluent builder APIs.</summary>
|
||||||
@@ -14627,6 +14564,11 @@ namespace DynamORM
|
|||||||
}
|
}
|
||||||
internal static class DynamicProcedureResultBinder
|
internal static class DynamicProcedureResultBinder
|
||||||
{
|
{
|
||||||
|
private sealed class ResultPropertyBinding
|
||||||
|
{
|
||||||
|
public ProcedureResultAttribute Attribute { get; set; }
|
||||||
|
public PropertyInfo Property { get; set; }
|
||||||
|
}
|
||||||
internal static bool IsProcedureContract(object item)
|
internal static bool IsProcedureContract(object item)
|
||||||
{
|
{
|
||||||
return item is IProcedureParameters;
|
return item is IProcedureParameters;
|
||||||
@@ -14641,9 +14583,10 @@ namespace DynamORM
|
|||||||
|
|
||||||
return iface == null ? null : iface.GetGenericArguments()[0];
|
return iface == null ? null : iface.GetGenericArguments()[0];
|
||||||
}
|
}
|
||||||
internal static bool CanReadResults(Type resultType)
|
internal static bool HasDeclaredResultBinding(Type resultType)
|
||||||
{
|
{
|
||||||
return resultType != null && typeof(IProcedureResultReader).IsAssignableFrom(resultType);
|
return resultType != null &&
|
||||||
|
(typeof(IProcedureResultReader).IsAssignableFrom(resultType) || GetResultPropertyBindings(resultType).Count > 0);
|
||||||
}
|
}
|
||||||
internal static object CreateDeclaredResult(Type resultType)
|
internal static object CreateDeclaredResult(Type resultType)
|
||||||
{
|
{
|
||||||
@@ -14658,11 +14601,17 @@ namespace DynamORM
|
|||||||
}
|
}
|
||||||
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
|
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
|
||||||
{
|
{
|
||||||
if (!CanReadResults(resultType))
|
if (!HasDeclaredResultBinding(resultType))
|
||||||
throw new InvalidOperationException(string.Format("Type '{0}' does not implement IProcedureResultReader.", resultType == null ? "<null>" : resultType.FullName));
|
throw new InvalidOperationException(string.Format("Type '{0}' does not declare a supported procedure result binding.", resultType == null ? "<null>" : resultType.FullName));
|
||||||
|
|
||||||
object instance = CreateDeclaredResult(resultType);
|
object instance = CreateDeclaredResult(resultType);
|
||||||
((IProcedureResultReader)instance).ReadResults(reader);
|
|
||||||
|
IList<ResultPropertyBinding> bindings = GetResultPropertyBindings(resultType);
|
||||||
|
if (bindings.Count > 0)
|
||||||
|
BindResultProperties(instance, reader, bindings);
|
||||||
|
else
|
||||||
|
((IProcedureResultReader)instance).ReadResults(reader);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
internal static object BindPayload(Type resultType, string mainResultName, object mainResult, IDictionary<string, object> returnValues, object existing = null)
|
internal static object BindPayload(Type resultType, string mainResultName, object mainResult, IDictionary<string, object> returnValues, object existing = null)
|
||||||
@@ -14688,6 +14637,210 @@ namespace DynamORM
|
|||||||
|
|
||||||
return payload.ToDynamic();
|
return payload.ToDynamic();
|
||||||
}
|
}
|
||||||
|
private static IList<ResultPropertyBinding> GetResultPropertyBindings(Type resultType)
|
||||||
|
{
|
||||||
|
return resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(x => x.CanWrite && x.GetIndexParameters().Length == 0)
|
||||||
|
.Select(x => new ResultPropertyBinding
|
||||||
|
{
|
||||||
|
Property = x,
|
||||||
|
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
|
||||||
|
})
|
||||||
|
.Where(x => x.Attribute != null)
|
||||||
|
.OrderBy(x => x.Attribute.ResultIndex)
|
||||||
|
.ThenBy(x => x.Property.MetadataToken)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
private static void BindResultProperties(object instance, IDataReader reader, IList<ResultPropertyBinding> bindings)
|
||||||
|
{
|
||||||
|
ValidateBindings(instance.GetType(), bindings);
|
||||||
|
|
||||||
|
int currentIndex = 0;
|
||||||
|
bool hasCurrent = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < bindings.Count; i++)
|
||||||
|
{
|
||||||
|
ResultPropertyBinding binding = bindings[i];
|
||||||
|
while (hasCurrent && currentIndex < binding.Attribute.ResultIndex)
|
||||||
|
{
|
||||||
|
hasCurrent = reader.NextResult();
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
if (!hasCurrent || currentIndex != binding.Attribute.ResultIndex)
|
||||||
|
break;
|
||||||
|
|
||||||
|
object value = ReadResultValue(binding.Property.PropertyType, binding.Attribute, reader);
|
||||||
|
binding.Property.SetValue(instance, value, null);
|
||||||
|
|
||||||
|
if (i + 1 < bindings.Count)
|
||||||
|
{
|
||||||
|
hasCurrent = reader.NextResult();
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void ValidateBindings(Type resultType, IList<ResultPropertyBinding> 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 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();
|
||||||
|
|
||||||
|
if (propertyType == typeof(Guid))
|
||||||
|
{
|
||||||
|
Guid g;
|
||||||
|
if (Guid.TryParse(value.ToString(), out g))
|
||||||
|
return g;
|
||||||
|
return propertyType.GetDefaultValue();
|
||||||
|
}
|
||||||
|
return propertyType.CastObject(value);
|
||||||
|
}
|
||||||
|
private static object ReadSimpleList(Type propertyType, Type elementType, ProcedureResultAttribute attr, IDataReader reader)
|
||||||
|
{
|
||||||
|
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 (elementType == typeof(Guid))
|
||||||
|
{
|
||||||
|
Guid g;
|
||||||
|
list.Add(Guid.TryParse(value.ToString(), out g) ? (object)g : elementType.GetDefaultValue());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
list.Add(elementType.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;
|
||||||
|
|
||||||
|
int ordinal = reader.GetOrdinal(attr.ColumnName);
|
||||||
|
if (ordinal < 0)
|
||||||
|
throw new IndexOutOfRangeException(attr.ColumnName);
|
||||||
|
|
||||||
|
return ordinal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// <summary>Framework detection and specific implementations.</summary>
|
/// <summary>Framework detection and specific implementations.</summary>
|
||||||
public static class FrameworkTools
|
public static class FrameworkTools
|
||||||
@@ -17684,6 +17837,72 @@ namespace DynamORM
|
|||||||
public class IgnoreAttribute : Attribute
|
public class IgnoreAttribute : Attribute
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>Declares mapping of a typed procedure result property to a specific result set.</summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ProcedureResultAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
/// <summary>Allows to add table name to class.</summary>
|
/// <summary>Allows to add table name to class.</summary>
|
||||||
[AttributeUsage(AttributeTargets.Class)]
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
public class TableAttribute : Attribute
|
public class TableAttribute : Attribute
|
||||||
@@ -18131,6 +18350,22 @@ namespace DynamORM
|
|||||||
_database = null;
|
_database = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
namespace TypedSql
|
namespace TypedSql
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -68,6 +68,39 @@ namespace DynamORM.Tests.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 ProcedureMultiResultArgs : IProcedureParameters<ProcedureMultiResult>
|
public class ProcedureMultiResultArgs : IProcedureParameters<ProcedureMultiResult>
|
||||||
{
|
{
|
||||||
[ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)]
|
[ProcedureParameter("status", Direction = ParameterDirection.Output, Order = 1, DbType = DbType.Int32)]
|
||||||
|
|||||||
@@ -172,6 +172,40 @@ namespace DynamORM.Tests.Procedure
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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]
|
[Test]
|
||||||
public void TestDeclaredResultPayloadCanAugmentReaderResult()
|
public void TestDeclaredResultPayloadCanAugmentReaderResult()
|
||||||
{
|
{
|
||||||
@@ -195,5 +229,13 @@ namespace DynamORM.Tests.Procedure
|
|||||||
CollectionAssert.AreEqual(new[] { "A" }, result.Codes);
|
CollectionAssert.AreEqual(new[] { "A" }, result.Codes);
|
||||||
CollectionAssert.AreEqual(new[] { 10 }, result.States);
|
CollectionAssert.AreEqual(new[] { 10 }, result.States);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeclaredResultBindingDetectionSupportsAttributedResults()
|
||||||
|
{
|
||||||
|
Assert.IsTrue(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureMultiResult)));
|
||||||
|
Assert.IsTrue(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureAttributedResult)));
|
||||||
|
Assert.IsFalse(DynamicProcedureResultBinder.HasDeclaredResultBinding(typeof(ProcedureParameterResult)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ namespace DynamORM
|
|||||||
|
|
||||||
object mainResult = null;
|
object mainResult = null;
|
||||||
|
|
||||||
if (types.Count == 0 && DynamicProcedureResultBinder.CanReadResults(declaredResultType))
|
if (types.Count == 0 && DynamicProcedureResultBinder.HasDeclaredResultBinding(declaredResultType))
|
||||||
{
|
{
|
||||||
using (IDataReader rdr = cmd.ExecuteReader())
|
using (IDataReader rdr = cmd.ExecuteReader())
|
||||||
using (IDataReader cache = rdr.CachedReader())
|
using (IDataReader cache = rdr.CachedReader())
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using DynamORM.Mapper;
|
using DynamORM.Mapper;
|
||||||
using DynamORM.Objects;
|
using DynamORM.Objects;
|
||||||
|
|
||||||
@@ -37,6 +38,12 @@ namespace DynamORM.Helpers
|
|||||||
{
|
{
|
||||||
internal static class DynamicProcedureResultBinder
|
internal static class DynamicProcedureResultBinder
|
||||||
{
|
{
|
||||||
|
private sealed class ResultPropertyBinding
|
||||||
|
{
|
||||||
|
public ProcedureResultAttribute Attribute { get; set; }
|
||||||
|
public PropertyInfo Property { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool IsProcedureContract(object item)
|
internal static bool IsProcedureContract(object item)
|
||||||
{
|
{
|
||||||
return item is IProcedureParameters;
|
return item is IProcedureParameters;
|
||||||
@@ -53,9 +60,10 @@ namespace DynamORM.Helpers
|
|||||||
return iface == null ? null : iface.GetGenericArguments()[0];
|
return iface == null ? null : iface.GetGenericArguments()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool CanReadResults(Type resultType)
|
internal static bool HasDeclaredResultBinding(Type resultType)
|
||||||
{
|
{
|
||||||
return resultType != null && typeof(IProcedureResultReader).IsAssignableFrom(resultType);
|
return resultType != null &&
|
||||||
|
(typeof(IProcedureResultReader).IsAssignableFrom(resultType) || GetResultPropertyBindings(resultType).Count > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static object CreateDeclaredResult(Type resultType)
|
internal static object CreateDeclaredResult(Type resultType)
|
||||||
@@ -72,11 +80,17 @@ namespace DynamORM.Helpers
|
|||||||
|
|
||||||
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
|
internal static object ReadDeclaredResult(Type resultType, IDataReader reader)
|
||||||
{
|
{
|
||||||
if (!CanReadResults(resultType))
|
if (!HasDeclaredResultBinding(resultType))
|
||||||
throw new InvalidOperationException(string.Format("Type '{0}' does not implement IProcedureResultReader.", resultType == null ? "<null>" : resultType.FullName));
|
throw new InvalidOperationException(string.Format("Type '{0}' does not declare a supported procedure result binding.", resultType == null ? "<null>" : resultType.FullName));
|
||||||
|
|
||||||
object instance = CreateDeclaredResult(resultType);
|
object instance = CreateDeclaredResult(resultType);
|
||||||
((IProcedureResultReader)instance).ReadResults(reader);
|
|
||||||
|
IList<ResultPropertyBinding> bindings = GetResultPropertyBindings(resultType);
|
||||||
|
if (bindings.Count > 0)
|
||||||
|
BindResultProperties(instance, reader, bindings);
|
||||||
|
else
|
||||||
|
((IProcedureResultReader)instance).ReadResults(reader);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,5 +117,232 @@ namespace DynamORM.Helpers
|
|||||||
|
|
||||||
return payload.ToDynamic();
|
return payload.ToDynamic();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IList<ResultPropertyBinding> GetResultPropertyBindings(Type resultType)
|
||||||
|
{
|
||||||
|
return resultType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(x => x.CanWrite && x.GetIndexParameters().Length == 0)
|
||||||
|
.Select(x => new ResultPropertyBinding
|
||||||
|
{
|
||||||
|
Property = x,
|
||||||
|
Attribute = x.GetCustomAttributes(typeof(ProcedureResultAttribute), true).Cast<ProcedureResultAttribute>().FirstOrDefault()
|
||||||
|
})
|
||||||
|
.Where(x => x.Attribute != null)
|
||||||
|
.OrderBy(x => x.Attribute.ResultIndex)
|
||||||
|
.ThenBy(x => x.Property.MetadataToken)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BindResultProperties(object instance, IDataReader reader, IList<ResultPropertyBinding> bindings)
|
||||||
|
{
|
||||||
|
ValidateBindings(instance.GetType(), bindings);
|
||||||
|
|
||||||
|
int currentIndex = 0;
|
||||||
|
bool hasCurrent = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < bindings.Count; i++)
|
||||||
|
{
|
||||||
|
ResultPropertyBinding binding = bindings[i];
|
||||||
|
while (hasCurrent && currentIndex < binding.Attribute.ResultIndex)
|
||||||
|
{
|
||||||
|
hasCurrent = reader.NextResult();
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCurrent || currentIndex != binding.Attribute.ResultIndex)
|
||||||
|
break;
|
||||||
|
|
||||||
|
object value = ReadResultValue(binding.Property.PropertyType, binding.Attribute, reader);
|
||||||
|
binding.Property.SetValue(instance, value, null);
|
||||||
|
|
||||||
|
if (i + 1 < bindings.Count)
|
||||||
|
{
|
||||||
|
hasCurrent = reader.NextResult();
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateBindings(Type resultType, IList<ResultPropertyBinding> 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 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();
|
||||||
|
|
||||||
|
if (propertyType == typeof(Guid))
|
||||||
|
{
|
||||||
|
Guid g;
|
||||||
|
if (Guid.TryParse(value.ToString(), out g))
|
||||||
|
return g;
|
||||||
|
return propertyType.GetDefaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
return propertyType.CastObject(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ReadSimpleList(Type propertyType, Type elementType, ProcedureResultAttribute attr, IDataReader reader)
|
||||||
|
{
|
||||||
|
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 (elementType == typeof(Guid))
|
||||||
|
{
|
||||||
|
Guid g;
|
||||||
|
list.Add(Guid.TryParse(value.ToString(), out g) ? (object)g : elementType.GetDefaultValue());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
list.Add(elementType.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;
|
||||||
|
|
||||||
|
int ordinal = reader.GetOrdinal(attr.ColumnName);
|
||||||
|
if (ordinal < 0)
|
||||||
|
throw new IndexOutOfRangeException(attr.ColumnName);
|
||||||
|
|
||||||
|
return ordinal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
DynamORM/Mapper/ProcedureResultAttribute.cs
Normal file
52
DynamORM/Mapper/ProcedureResultAttribute.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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)]
|
||||||
|
public class ProcedureResultAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user