From 79cce3d1f4e063a0bf385daba1d7f2cebb420c20 Mon Sep 17 00:00:00 2001 From: Grzegorz Russek Date: Fri, 27 Feb 2026 14:26:16 +0100 Subject: [PATCH] Add object-based stored procedure parameter binding --- AmalgamationTool/DynamORM.Amalgamation.cs | 418 +++++++++++++----- DynamORM.Tests/Helpers/FakeDbCommand.cs | 135 ++++++ .../Helpers/ProcedureParameterModels.cs | 31 ++ .../ProcedureParameterBinderTests.cs | 117 +++++ DynamORM/DynamicProcedureInvoker.cs | 339 +++++++------- .../DynamicProcedureParameterBinder.cs | 175 ++++++++ DynamORM/ProcedureParameterAttribute.cs | 84 ++++ 7 files changed, 1017 insertions(+), 282 deletions(-) create mode 100644 DynamORM.Tests/Helpers/FakeDbCommand.cs create mode 100644 DynamORM.Tests/Helpers/ProcedureParameterModels.cs create mode 100644 DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs create mode 100644 DynamORM/Helpers/DynamicProcedureParameterBinder.cs create mode 100644 DynamORM/ProcedureParameterAttribute.cs diff --git a/AmalgamationTool/DynamORM.Amalgamation.cs b/AmalgamationTool/DynamORM.Amalgamation.cs index 7fb9fe2..ab08dbc 100644 --- a/AmalgamationTool/DynamORM.Amalgamation.cs +++ b/AmalgamationTool/DynamORM.Amalgamation.cs @@ -5377,133 +5377,110 @@ namespace DynamORM 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 +5492,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); + } } } } @@ -6826,6 +6835,54 @@ namespace DynamORM #endregion IExtendedDisposable Members } + /// 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; + } + } namespace Builders { /// Typed join kind used by typed fluent builder APIs. @@ -14416,6 +14473,133 @@ namespace DynamORM return resultTable; } } + 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 (item == null) + return false; + + Type type = item.GetType(); + + if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(Guid) || type == typeof(TimeSpan)) + return false; + + return GetBindableProperties(type).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; + } + } /// Framework detection and specific implementations. public static class FrameworkTools { 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 _items = new List(); + + 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); + } + } +} diff --git a/DynamORM.Tests/Helpers/ProcedureParameterModels.cs b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs new file mode 100644 index 0000000..1581645 --- /dev/null +++ b/DynamORM.Tests/Helpers/ProcedureParameterModels.cs @@ -0,0 +1,31 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System.Data; + +namespace DynamORM.Tests.Helpers +{ + public class ProcedureParameterObject + { + [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 + { + [DynamORM.Mapper.Column("code", false, DbType.String, 64)] + public string Code { get; set; } + } +} diff --git a/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs new file mode 100644 index 0000000..dc43bab --- /dev/null +++ b/DynamORM.Tests/Procedure/ProcedureParameterBinderTests.cs @@ -0,0 +1,117 @@ +/* + * DynamORM - Dynamic Object-Relational Mapping library. + * Copyright (c) 2012-2026, Grzegorz Russek (grzegorz.russek@gmail.com) + * All rights reserved. + */ + +using System.Data; +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); + } + } + } +} diff --git a/DynamORM/DynamicProcedureInvoker.cs b/DynamORM/DynamicProcedureInvoker.cs index 7758da2..dff15de 100644 --- a/DynamORM/DynamicProcedureInvoker.cs +++ b/DynamORM/DynamicProcedureInvoker.cs @@ -31,13 +31,13 @@ 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; + +namespace DynamORM +{ /// Dynamic procedure invoker. /// Unfortunately I can use out and ref to /// return parameters, . @@ -106,164 +106,173 @@ namespace DynamORM #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(); - 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(); - 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(); - 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 (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(); + 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(); + 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(); + 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 diff --git a/DynamORM/Helpers/DynamicProcedureParameterBinder.cs b/DynamORM/Helpers/DynamicProcedureParameterBinder.cs new file mode 100644 index 0000000..65960f6 --- /dev/null +++ b/DynamORM/Helpers/DynamicProcedureParameterBinder.cs @@ -0,0 +1,175 @@ +/* + * 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 ReturnParameters { get; set; } + } + + internal static bool CanBind(object item) + { + if (item == null) + return false; + + Type type = item.GetType(); + + if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(Guid) || type == typeof(TimeSpan)) + return false; + + return GetBindableProperties(type).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; + } + } +} diff --git a/DynamORM/ProcedureParameterAttribute.cs b/DynamORM/ProcedureParameterAttribute.cs new file mode 100644 index 0000000..c45586b --- /dev/null +++ b/DynamORM/ProcedureParameterAttribute.cs @@ -0,0 +1,84 @@ +/* + * 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; +using DynamORM.Mapper; + +namespace DynamORM +{ + /// 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; + } + } +}