使用 DynamicMethods 进行快速声明式类型安全, 用于 SQL
生成类型化字段访问器并将其应用于对象映射和 SQL 存储过程
引言
增强现有的大型代码库本身就带来一系列挑战,其中一个极其痛苦的问题是,当代码为了短期便利而牺牲类型安全时,尤其是在各种形式的对象到数据库和对象到数据网格映射方面。
本文利用一些基于稀疏文档化`TypedReference`和`DynamicMethod`类型的技术,重新引入类型安全。
- 完全编译的代码,用于设置、获取和规范化类的公共字段或结构而不进行装箱;
- 对可空类型进行特殊处理,包括用户定义的结构和“嵌入式”空值;
- 规范化空字符串、NaN、Infinity、DateTime.MinValue 等;
- 用于调用 SQL Server 存储过程和读取结果的声明式类型安全包装器;
- 对表值参数进行优化支持,消除了通常所需的低效`DataTable`;
- 完全支持 SQL Server T-SQL 类型规则,包括处理 NaN 等亚正常浮点数;
- 轻量级数据库管理器类,支持通过`CancellationToken`取消所有 SQL 调用。
动机
本节展示了一些来自数据库方面的示例,然后是一些性能数据。
简单示例
给定一个带有输出参数的存储过程
PROCEDURE dbo.uspGetNVPIds
@idCount int = 1,
@first_value bigint OUTPUT,
@increment bigint OUTPUT
然后声明一个映射结构(字段顺序必须匹配)
public struct uspGetNVPIds
{
public int idCount;
[DB.Parameter(ParameterDirection.Output)]
public long first_value, increment;
}
并通过一个简单的数据库管理器`DB`进行调用。
//Cancellation support
var token = CancellationToken.None;
//Ask for next 5 ids from the sequence
//1. Setup
var args = new DB.Parameters<uspGetNVPIds>("dbo.uspGetNVPIds");
args.Data.idCount = 5;
//2. Execute
DB.ExecuteNonQuery(token, ref args);
//3. Read outputs
var first_value = args.Data.first_value;
var increment = args.Data.increment;
复杂示例
给定一个带有一个结果集且无参数的存储过程
PROCEDURE dbo.uspNVPLoad AS
select d.Id, d.Name, d.Value, d.updatedAt, d.updatedBy,
CAST(d.[version] AS BIGINT) as [Version]
from tNVPData d
然后声明一个映射结构(字段顺序无关紧要)
public struct uspNVPLoad
{
public long Id;
public string Name, Value;
public long Version;
//Skip the other values (updatedAt, updatedBy)
}
并再次通过`DB`进行调用。
//Cancellation support
var token = CancellationToken.None;
//1. Setup
//Nothing to do because there are no arguments
//2. Execute
using (var reader = DB.ExecuteReader<uspNVPLoad>(token, "dbo.uspNVPLoad"))
{
while (reader.Read())
{
//These will matchup by name
var id = reader.Row.Id;
var name = reader.Row.Name;
var value = reader.Row.Value;
var dbVersion = reader.Row.Version;
//Do something with them
}
}
以及痛苦示例
给定一个典型的手动编写的批量更新器和相应的 UDT
CREATE TYPE dbo.udtNVPairs AS TABLE
(
Id BIGINT NOT NULL PRIMARY KEY,
TraceId INT,
Name NVarChar(255) NOT NULL,
Value NVarChar(255) NOT NULL
)
PROCEDURE dbo.uspNVPSaveMany
@values dbo.udtNVPairs readonly,
@updatedBy nvarchar(16) OUTPUT,
@updatedAt smalldatetime OUTPUT
AS
--ETC
OUTPUT src.TraceId, cast(inserted.[version] as bigint) as [Version]
然后声明映射结构。
public struct udtNVPairs
{
//Order and type must match UDT but names can be anything
public long Id;
public int TraceId;
public string Name, Value;
}
public struct uspNVPSaveMany
{
//TVP
public DB.Table<udtNVPairs> values;
[DB.Parameter("@updatedBy", ParameterDirection.Output)]
public string LastSavedBy;
[DB.Parameter("@updatedAt", ParameterDirection.Output)]
public DateTime LastSavedAtUtc;
public struct Result
{
public int TraceId;
public long Version;
}
}
并再次通过`DB`进行调用。
//Cancellation support
var token = CancellationToken.None;
//Create the TVP data
var table = new DB.Table<udtNVPairs>(Data.Count);
for (int i = 0; i < Data.Count; i++)
{
//So can match up date with records later
table.Row.TraceId = i;
table.Row.Name = "Data";
//ETC
//Commit the new row
table.RowCommitAsNew();
}
//1. Setup
var args = new DB.Parameters<uspNVPSaveMany>("dbo.uspNVPSaveMany");
args.Data.values = table;
//2. Execute
using (var reader = DB.ExecuteReader<uspNVPSaveMany.Result, uspNVPSaveMany>(token, ref args))
{
while (reader.Read())
{
//These will matchup by name
var traceId = reader.Row.TraceId;
var version = reader.Row.Version;
//ETC
}
}
//Want output parameters so transfer them once reader has closed
args.ReadAnyOutputs();
var lastSavedBy = args.Data.LastSavedBy;
var lastSavedAt = args.Data.LastSavedAtUtc;
规范化
这发生在每次设置和获取时,所有字段都将被规范化为其主值,包括嵌入在可空类型中的值,如下所示:
类型 | 主(Primary) | 替代方案 |
---|---|---|
float |
NaN | 正无穷大、负无穷大 |
double |
NaN | 正无穷大、负无穷大 |
char |
'\0' | |
日期时间 |
default(DateTime) |
<= DateTime.MinValue |
long |
long.MaxValue |
|
int |
int.MaxValue |
|
decimal |
decimal.MaxValue |
|
字符串 |
null |
string.Empty |
请注意,此表是关于`DynamicField`类;SQL 之上的附加包装器也将以适当的方式处理必需的`string`字段,将其规范化为`string.Empty`。
基本规则是,如果某项匹配,则认为它是空的,因此返回结果将是主空值。
以下示例将根据上述内容进行规范化,然后 SQL 包装器将发送`DBNull`(请记住 SQL 不允许亚正常值)。
struct SubNormals
{
[FieldOptional]
public double One = double.PositiveInfinity;
public double? Two = double.NegativeInfinity;
//This will throw before it is passed to the SQL
public double Required = double.NaN;
}
上述映射在金融应用中至关重要,因为亚正常值很常见。当处理大量此类计算时,最好避免使用可空类型,因此推荐使用带有属性的方法。
性能
很难获得可重复的计时,特别是`DynamicMethods`在 AppDomain 中的首次使用的“启动”时间,因此这里忽略了这些值。此外,仅为非数据库示例提供计时,以避免 ADO.NET 延迟访问行为的影响。
来自示例的原始非类型安全方法和新方法的计时如下:
AddingTypeSafety
- 新方法的设置时间为 42 毫秒,原始方法仅为 8 毫秒,但两者后续运行均为 0 毫秒。DTOsToDataGrid
- 新方法的设置时间为 37 毫秒,原始方法仅为 12 毫秒,但两者后续运行均为 0 毫秒。
结论
使用此方法的设置速度大约是原来的四倍,但编译后的代码和元数据缓存使得后续运行与直接手动编码一样快。请注意,数据库方面的性能也会相似,但`Reader`每次设置可能稍慢一些。
请注意,由于 .NET 针对字段而不是属性进行了优化(例如,您无法获取属性的地址),并且由于此解决方案使用了`struct`并避免了 Linq,因此它在生成的 IL 代码简洁和(几乎为零的)堆使用方面都具有极高的内存效率。
对象映射
本节包含两个真实的实时代码示例,它们围绕`object`而不是更类型安全的方法构建,这些是上面性能测试的核心。
对象上的字段
现有代码维护着对象的 OO 层级结构,但使用基本的非类型安全对象包作为字段存储,如下所示:
//Existing code is using untyped objects indexed by an enumeration
public enum FieldName
{
FieldString,
FieldDouble,
FieldDateTime,
}
//Assume a lot of these kinds of collections exist within the OO hierarchy
readonly IDictionary<FieldName, object> Fields =
new SortedList<FieldName, object>
{
{FieldName.FieldString, "MyFieldString"},
{FieldName.FieldDouble, 1.23},
//FieldDateTime is not present
};
//Existing accessors look like this
public T GetFieldAs<T>(FieldName name) where T : class
{
object res;
Fields.TryGetValue(name, out res);
return res as T;
}
在 OO 层级结构的各种方法中,该方法所需的每个字段都需要提取,如下所示:
public bool OldExtractValues(out FieldName? errorField)
{
var firstField = GetFieldAs<string>(FieldName.FieldString);
if (string.IsNullOrEmpty(firstField))
{
errorField = FieldName.FieldString;
return false;
}
var secondField = GetField<double>(FieldName.FieldDouble);
if (!secondField.HasValue)
{
errorField = FieldName.FieldDouble;
return false;
}
//Repeat the above for each field needed
//Now use the fields to do some business task
//e.g.
var someBusinessTask = firstField + secondField.ToString();
}
这可以被一个`struct`替换,该结构在 OO 层级结构中带有标记的字段,如下所示:
public struct Values
{
public string FieldString;
public double FieldDouble;
[FieldOptional]
public DateTime FieldDateTime;
}
然后方法可以简化为专注于业务逻辑。
public static bool NewExtractValues(out FieldName? errorField)
{
//All structs so no GC pressure
var values = new Extractor<Values>();
if (!values.Extract(Fields, out errorField)) return false;
//Now use the fields to do some business task
//e.g.
var someBusinessTask = values.Fields.FieldString + values.Fields.FieldDouble.ToString();
}
这段共享例程封装了本文的`DynamicFields`类。
//Common helper
public struct Extractor<T> where T : struct
{
public T Fields;
public bool Extract(IDictionary<FieldName, object> fields, out FieldName? errorField)
{
//Instance rather than using static in order to easily search by name
var dynamic = new DynamicFields<T>();
var ptr = __makeref(Fields);
foreach (var field in dynamic)
{
//Will throw if T has a field that is not a valid FieldName enum member
var key = Enum<FieldName>.Parse(field.Name);
object value;
fields.TryGetValue(key, out value);
var code = field.TrySetUntyped(ptr, value);
if (code > DynamicField.Code.IsEmpty)
{
errorField = key;
return false;
}
}
errorField = null;
return true;
}
}
字段到数据网格
现有代码有一个数据提供程序,它发出 OO 层级结构中的DTO。同名字段通常具有相同的类型,而代码的其他部分(例如数据网格)则期望装箱的规范化值。
//Existing DTOs
public abstract class DTO
{
//etc
}
public class Data1DTO : DTO
{
public string FieldString;
[FieldOptional]
public double FieldOptionalDouble;
public int FieldInteger;
public Data1DTO()
: base("Data1")
{
}
}
public class Data2DTO : DTO
{
[FieldOptional]
public string FieldString;
public DateTime FieldDate;
//etc
}
//Assume a lot of these came from a data provider
readonly DTO[] Data = new DTO[]
{
new Data1DTO { FieldString = "Data1Field1", FieldInteger = 1,
FieldOptionalDouble = 1.23 },
new Data1DTO { FieldString = "Data1Field2", FieldInteger = 2,
FieldOptionalDouble = double.NaN },
new Data2DTO { FieldString = "Data2Field1", FieldDate = DateTime.Today },
new Data2DTO { FieldDate = DateTime.Today.AddDays(7) },
};
已存在一个地图,该地图是在运行时使用反射或在编译时使用T4等生成的。
public enum OldFieldNames
{
FieldString,
FieldInteger,
FieldDate,
FieldOptionalDouble,
};
static readonly IDictionary<int, IDictionary<OldFieldNames, Func<DTO, object>>> OldMap
= new SortedList<int, IDictionary<OldFieldNames, Func<DTO, object>>>()
{
{typeof(Data1DTO).MetadataToken, new SortedList<OldFieldNames, Func<DTO, object>>{
{OldFieldNames.FieldString, dto => ((Data1DTO)dto).FieldString},
{OldFieldNames.FieldInteger, dto => ((Data1DTO)dto).FieldInteger},
{OldFieldNames.FieldOptionalDouble, dto => ((Data1DTO)dto).FieldOptionalDouble},
}},
{typeof(Data2DTO).MetadataToken, new SortedList<OldFieldNames, Func<DTO, object>>{
{OldFieldNames.FieldString, dto => ((Data2DTO)dto).FieldString},
{OldFieldNames.FieldDate, dto => ((Data2DTO)dto).FieldDate},
}},
};
获取字段和列等的现有方法大致如下(有关详细信息,请参阅示例代码)。
public static object OldGetField(DTO dto, OldFieldNames field)
{
object res = null;
IDictionary<OldFieldNames, Func<DTO, object>> fields;
if (OldMap.TryGetValue(dto.TypeId, out fields))
{
Func<DTO, object> extractor;
if (fields.TryGetValue(field, out extractor))
{
res = extractor(dto);
}
}
return res;
}
public static object[] OldGetColumn(OldFieldNames field)
{
var res = new object[Data.Length];
//etc
}
public static object[][] OldGetMatrix(params OldFieldNames[] fields)
{
var res = new object[fields.Length][];
//etc
}
使用本文`DynamicFields`类的一种更简洁的方法如下。请注意,生成下面`NewMap`的选项仍然是开放的。
static IDictionary<int, DynamicFields> NewMap;
static ISet<string> NewFieldNames;
static void NewCreateMap()
{
//Done here so it can be timed
NewMap = new SortedList<int, DynamicFields>()
{
{typeof(Data1DTO).MetadataToken, new DynamicFields(typeof(Data1DTO), typeof(DTO))},
{typeof(Data2DTO).MetadataToken, new DynamicFields(typeof(Data2DTO), typeof(DTO))},
};
//Create the unique field list
NewFieldNames = new SortedSet<string>();
foreach (var pair in NewMap)
{
foreach (var field in pair.Value)
{
NewFieldNames.Add(field.Name);
}
}
}
现在可以一步完成提取和规范化。
public static object NewGetField(DTO dto, DynamicField.SearchKey key)
{
object res = null;
DynamicFields fields;
if (NewMap.TryGetValue(dto.TypeId, out fields))
{
DynamicField field;
if (fields.TryGet(key, out field) >= 0)
{
//Use the templated approach to hide the public use of TypedReferences
res = field.GetUntyped(dto);
}
}
return res;
}
//Other routines are similar to before
DynamicField
这是该方法的核心,负责解析`Type`并指示`DynamicGenerator`发出`DynamicMethods`所需的 IL。它查找每个公共字段上的以下一个或多个属性:
FieldOptionalAttribute
- 在源中包含一个。
缓存值
类在构造后提供以下数据:
// Short name of the field owner
public readonly string ParentName;
// Short name of the field
public string Name { get; protected set; }
// Type of the field owner
public readonly Type ParentType;
// Type of the instance used to access this field.
// Normally same as ParentType but for fields on derived classes this will be the
// base class type if a base class instance will be used to access them.
public readonly Type RootType;
// Type of this field
public readonly Type Type;
// FieldInfo of this field
public readonly FieldInfo Field;
// Type of this field. If it was nullable, this is the underlying type
public readonly Type TypeNormalised;
// If IsGeneric is the Generic template otherwise null
public readonly Type TypeGenericTemplate;
// TypeCode of this field. If it was nullable, this is the underlying type's code.
public readonly TypeCode TypeCodeNormalised;
// Field is nullable (see IsNullableType).
// Or field is: custom struct with HasValue property and the FieldOptional attribute (see IsUDT).
// Or field is: DateTime/double/char/etc/reference-type with the FieldOptional attribute (see IsValueType).
public readonly bool IsOptional;
// Type is System.Nullable based
public readonly bool IsNullableType;
// Is an embedded structure
public readonly bool IsUDT;
// Any attributes, empty array if none
public readonly Attribute[] Attributes;
结果码
设置器和获取器可以返回代码,这些代码考虑了该类型的空值定义以及它在语义上是否可选。
// The result code for advanced getting/setting
public enum Code
{
// Success - set/get was not empty (double, int, char, DateTime, etc treated specially, otherwise, means not null or HasValue)
Ok = 0,
// Success - set/get was empty (double, int, char, DateTime, etc treated specially, otherwise, means null or not HasValue).
// When setting/getting, value is normalised to single empty representation.
IsEmpty,
// Failure - set/get failed due to type mismatch
TypeMismatch,
// Success - set/get was empty (double, int, char, DateTime, etc treated specially, otherwise, means null or not HasValue).
// However, the field itself is required.
NotOptional,
}
获取器
获取器存在专门的类型化和非类型化版本,并依赖于`TypedReferences`。
public Code TryGetUntyped(TypedReference root, out object res)
{
var code = _GetterBoxed(root, out res);
return MapResult(code);
}
public Code TryGet(TypedReference root, out T res)
{
var code = _Getter(root, out res);
return MapResult(code);
}
Setters
设置器也类似。
public Code TrySetUntyped(TypedReference root, object value)
{
var code = _SetterBoxed(root, value);
return MapResult(code);
}
public Code TrySet(TypedReference root, T value)
{
var code = _Setter(root, value);
return MapResult(code);
}
什么是 TypedReference?
官方的TypedReference只是一个托管指针,并带有被指向值的`Type`。它们是 IL 世界的一等公民,但在 C# 语言中仍然很大程度上未被文档化,尽管它支持它们。
C#
C# 语言对其支持的一个示例更有帮助:
struct Test1
{
public double Field;
}
struct Test2
{
public Test1 Embedded;
}
static void Test()
{
Test2 instance = new Test2();
//0. Work with the embedded field
TypedReference ptr = __makeref(instance.Embedded.Field);
//1. Can extract type (double)
Type typeOfPtr = __reftype(ptr);
//2. Can read value
double myValue = __refvalue(ptr, double);
//3. Can write value
__refvalue(ptr, double) = 1.35;
}
IL
请注意,没有装箱/拆箱,并且使用`mkrefany`和`refanyval`操作码,这会直接编译成简洁、可验证的 IL。
.locals init ([0] valuetype Test2 'instance', [1] typedref ptr)
ldloca.s 'instance'
initobj Test2
//ptr = __makeref(instance.Embedded.Field)
ldloca.s 'instance'
ldflda valuetype Test1 Test2::Embedded
ldflda float64 Test1::Field
mkrefany System.Double
stloc.1
//= __refvalue(ptr, double)
ldloc.1
refanyval System.Double
ldind.r8
//__refvalue(ptr, double) = 1.35
ldloc.1
refanyval System.Double
ldc.r8 1.3500000000000001
stind.r8
DynamicGenerator
它负责生成`DynamicMethods`并为`DynamicFields`后续使用创建委托。一些亮点包括:
动态方法
在`CreateMethod`中需要注意的主要事项是,在创建DynamicMethod时,对 a) 什么类型是可见的以及 b) 方法中的 IL 是否必须是可验证的,存在一套复杂的安全规则。最好的方法是中和这两个问题,如下所示:
static MethodInfo CreateMethod(out ILGenerator ilg, string name, Type returnType, Type rootType = null, params Type[] args)
{
//If a type rootType or explicit module are not specified then the security restricts the use of unverifiable IL etc.
var moduleForSecurity = rootType != null ? rootType.Module : typeof(DynamicGenerator).Module;
//Skipping all visibility checks allows more flexibility in accessing private nested types for example
var res = new DynamicMethod(name, returnType, args, moduleForSecurity, skipVisibility: true);
ilg = res.GetILGenerator();
return res;
}
static Delegate CreateDelegate(Type type, MethodInfo method)
{
return ((DynamicMethod)method).CreateDelegate(type);
}
快速创建者
能够即时创建类实例而无需使用反射很有用,即使它们具有带参数的非公共构造函数。`CreateCreator<>`及其相关重载执行此任务,所有任务最终都汇总到此处。
static Delegate _CreateCreator(Type @delegate, string name, Type retType, Type typeToCreate, Type rootType, params Type[] constructorArgs)
{
ILGenerator il;
var res = CreateMethod(out il, name, retType, rootType, constructorArgs);
_EmitCreator(il, constructor, retType);
return CreateDelegate(@delegate, res);
}
IL 的生成通常比Linq 表达式替代方案更优雅且易于理解。
static void _EmitCreator(ILGenerator il, ConstructorInfo constructor, Type typeToReturn)
{
var typeToCreate = constructor.DeclaringType;
var argc = constructor.GetParameters().Length;
for (int i = 0; i < argc; i++) il.Emit(OpCodes.Ldarg, i);
il.Emit(OpCodes.Newobj, constructor);
if (typeToReturn != null && typeToReturn != typeToCreate) il.Emit(OpCodes.Castclass, typeToReturn);
il.Emit(OpCodes.Ret);
}
使用回调
使用回调方法来允许将不同的类型化例程组合在一起,而无需通过 IL 生成所有内容。API 与通常的 Getter 略有不同,并且大量使用`TypedReferences`。
public Code TryGet(TypedReference root, DynamicGenerator.DelegateGetterCallback callback, TypedReference state)
{
var code = _GetterViaCallback(root, callback, state);
return MapResult(code);
}
例如,假设有一组预定义的委托,每个委托处理一种特定类型:
struct myState
{
public Delegate SomeRoutineThatTakesATypedReference;
}
void MyGetterCallback(TypedReference state, TypedReference value, DynamicGenerator.Result code)
{
//Do nothing on failure
if (code == DynamicGenerator.Result.TypeMismatch) return;
//Access myState
myState typedState = __refvalue(state, myState);
//And let it process the value
typedState.SomeRoutineThatTakesATypedReference(value);
}
然后可以创建像这样的例程,它接受实例(通过常规的`TypedReference`)以及上面的`DynamicField`,选择正确的委托,并间接导致规范化后的值被发送给它。
void Test(TypedReference root, DynamicField field)
{
var state = new myState();
state.SomeRoutineThatTakesATypedReference = null; //Set delegate based on type of this field
//And access field
var code = field.TryGet(root, MyGetterCallback, __makeref(state));
}
请注意,这种方法是 SQL 包装器在不进行装箱的情况下处理与 SQL 类之间值读写方式的核心。
SQL 包装器
将上述技术应用于创建 SQL 包装器涉及大量对 SQL ADO.NET 提供程序的逆向工程代码的深入研究。这产生了三个类:`DB.Parameters`用于`SqlParameters`,`DB.Table`用于表值参数,`DB.Reader`用于`DbReader`本身。
DB.Field
`DB.Field`类建立在`DynamicField`和`DynamicGenerator`的基础上。它查找每个公共字段上的以下一个或多个附加属性:
SqlFacetAttribute
- 主要用于控制字符串长度;DB.ParameterAttribute
- 用于控制`SqlParameter`的方向和名称。
它包含以下信息:
// The SQL TVP position
public readonly int Ordinal;
// The field name
public string Name { get; protected set; }
// The backing dynamic field
public readonly DynamicField Binder;
// The name of the sql parameter (use DB.ParameterAttribute/DB.ResultAttribute to customise)
public string SqlName;
// The sql db type
public readonly SqlDbType SqlType;
// Where applicable, the max character length (use SqlFacetAttribute on Strings for e.g.)
public readonly int SqlMaxLength;
// The direction of the parameter - see DB.ParameterAttribute
public readonly ParameterDirection SqlDirection;
// If this is an input or inputoutput parameter
public bool IsInput { get { return (((int)SqlDirection) & 1) != 0; } }
// If this is an output or resultvalue parameter
public bool IsOutput { get { return !IsInput; } }
// If this is a table valued parameter
public bool IsTVP { get { return SqlType == SqlDbType.Structured; } }
前面描述的回调技术用于将值设置到`SqlDataRecord`中,并为给定类型化值创建正确的`SqlType`。这是前者回调:
void SetValueCallback(TypedReference stateRef, TypedReference valueRef, DynamicGenerator.Result code)
{
//Ignore if empty
if (code == DynamicGenerator.Result.Ok)
{
var state = __refvalue(stateRef, StateForDataRecord);
state.Setter(state, valueRef);
}
}
后者的回调涉及将(装箱的)`SqlType`的创建存储回状态结构中。
void CreateParameterCallback(TypedReference stateRef, TypedReference valueRef, DynamicGenerator.Result code)
{
//Ignore if empty
if (code == DynamicGenerator.Result.Ok)
{
var state = __refvalue(stateRef, StateForParameter);
__refvalue(stateRef, StateForParameter).SqlValue = state.Creator(state, valueRef);
}
}
然后将其暴露给其他`DB`类,以提取装箱的`SqlType`字段,如下所示:
// Gets the current field value and if not empty creates a (boxed) Sql type
// Result will be null if value was empty but result code can be used instead.
// Note that required strings/char that are empty will be set to Sql empty strings and not reported as NotOptional.
// Returns the result code.
internal DynamicField.Code TryCreateSqlValue(TypedReference source, out object sqlValue)
{
var state = new StateForParameter
{
IsNullable = Binder.IsNullableType,
Creator = _map[Binder.TypeCodeNormalised].Value2,
SqlValue = null
};
var res = Binder.TryGet(source, CreateParameterCallback, __makeref(state));
if (res == DynamicField.Code.NotOptional && SqlType == SqlDbType.NVarChar)
{
//String.Empty has special meaning for non nullable sql string fields
sqlValue = _SqlStringEmptyBoxed;
return DynamicField.Code.Ok;
}
sqlValue = state.SqlValue;
return res;
}
DB.Parameters<T>
这个泛型`struct`包装了一个标记过的结构,并使用`DB.Fields`从`SqlParameters`读取/写入值。关键例程的粗略概述如下(有关详细信息,请参阅源代码):
internal SqlParameter[] CreateParameters()
{
_parameters = new SqlParameter[_metaData.Length];
var ptr = __makeref(Data);
for (int i = 0; i < _metaData.Length; i++)
{
var info = _metaData[i];
var sql = _parameters[i];
//... etc ...
if (info.IsTVP)
{
code = info.Binder.TryGetUntyped(ptr, out value);
//Cannot be set to DBNull
if (code == DynamicField.Code.Ok)
{
sql.Value = (IEnumerable<SqlDataRecord>)value;
continue;
}
}
else
{
code = info.TryCreateSqlValue(ptr, out value);
if (code == DynamicField.Code.IsEmpty)
{
//TODO: Setting value to null is different from setting SqlValue to null:
//Use Value to send a NULL value as the value of the parameter.
//Use null or do not set SqlValue to use the default value for the parameter
sql.Value = null;
continue;
}
if (code == DynamicField.Code.Ok)
{
sql.SqlValue = value;
continue;
}
}
//... etc ...
}
}
DB.Reader<T>
这个泛型`struct`包装了一个标记过的结构,并使用`DB.Fields`从底层`DbReader`流式传输值。关键例程的粗略概述如下(有关详细信息,请参阅源代码):
public bool Read()
{
//... etc ...
var ptr = __makeref(Row);
var state = new StateForGetter();
for (int i = 0; i < _metaData.Length; i++)
{
var md = _metaData[i];
if (_reader.IsDBNull(i))
{
if (md.IsOptional)
{
code = md.TrySetUntyped(ptr, null);
}
else
{
code = DynamicField.Code.NotOptional;
}
}
else
{
state.Ordinal = i;
state.Field = md;
code = _map[md.TypeCodeNormalised](state, ptr);
//... etc ...
}
if (code <= DynamicField.Code.IsEmpty) continue;
//... etc ...
}
}
这里使用的方法依赖于泛型和 .NET `TypeCode`来选择要调用的正确委托。请注意`TypedReference`是如何简单地传递给`DynamicField`的。
//Taken from SqlMetaData InferFromValue and the Constructors
static readonly IDictionary<TypeCode, SqlGetterDelegate> _map =
new SortedList<TypeCode, SqlGetterDelegate>
(Enum<TypeCode>.Count, Enum<TypeCode>.Comparer)
{
{TypeCode.Boolean, (s, tr) => s.Set<bool> (tr, s.Reader.GetBoolean(s.Ordinal))},
{TypeCode.Byte, (s, tr) => s.Set<byte> (tr, s.Reader.GetByte(s.Ordinal))},
{TypeCode.Char, (s, tr) => s.Set<char> (tr, s.Reader.GetString(s.Ordinal)[0])},
{TypeCode.DateTime, (s, tr) => s.Set<DateTime>(tr, s.Reader.GetDateTime(s.Ordinal))},
{TypeCode.Decimal, (s, tr) => s.Set<decimal> (tr, s.Reader.GetDecimal(s.Ordinal))},
{TypeCode.Double, (s, tr) => s.Set<double> (tr, s.Reader.GetDouble(s.Ordinal))},
{TypeCode.Int16, (s, tr) => s.Set<short> (tr, s.Reader.GetInt16(s.Ordinal))},
{TypeCode.Int32, (s, tr) => s.Set<int> (tr, s.Reader.GetInt32(s.Ordinal))},
{TypeCode.Int64, (s, tr) => s.Set<long> (tr, s.Reader.GetInt64(s.Ordinal))},
{TypeCode.Single, (s, tr) => s.Set<float> (tr, s.Reader.GetFloat(s.Ordinal))},
{TypeCode.String, (s, tr) => s.Set<string> (tr, s.Reader.GetString(s.Ordinal))},
};
delegate DynamicField.Code SqlGetterDelegate(StateForGetter state, TypedReference tr);
struct StateForGetter
{
public SqlDataReader Reader;
public int Ordinal;
public DynamicField Field;
public DynamicField.Code Set<FT>(TypedReference ptr, FT value)
{
return ((DynamicField<FT>)Field).TrySet(ptr, value);
}
}
DB.Table<T>
这个泛型`struct`包装了一个行,并使用`DB.Fields`存储值以便稍后回放给`SqlDataRecord` - 即,这是一个表值参数。请注意,常规方法使用低效的`DataTable`,但这依赖于行的`List
以下是粗略概述(有关详细信息,请参阅源代码):
public class Table<T> : IEnumerable<SqlDataRecord>, IEnumerator<SqlDataRecord>
{
// The current row when enumerating.
// The new row to fill in when adding.
public T Row;
readonly List<T> _rows;
readonly DB.Field[] _metaData;
int _current;
//... etc ...
DB.Field.SqlDataRecordExtended _record;
Table<T> PrepareSqlDataRecord()
{
if (_record == null)
{
var count = _metaData.Length;
var md = new SqlMetaData[count];
for (int i = 0; i < count; i++)
{
DB.Field info = _metaData[i];
var m = info.SqlMaxLength != 0 ?
new SqlMetaData(info.Name, info.SqlType, info.SqlMaxLength)
: new SqlMetaData(info.Name, info.SqlType);
//Sql ordinal differs from index of field in this structure
var ord = info.Ordinal;
md[ord] = m;
}
_record = new DB.Field.SqlDataRecordExtended(md);
}
ReadReset();
return this;
}
bool System.Collections.IEnumerator.MoveNext()
{
if (ReadMoveNext())
{
var ptr = __makeref(Row);
for (int i = 0; i < _metaData.Length; i++)
{
DB.Field info = _metaData[i];
var code = info.TrySetValue(_record, ptr);
if (code == DynamicField.Code.Ok) continue;
if (code == DynamicField.Code.IsEmpty)
{
//Re-using record so have to reset value
_record.SetDBNull(info.Ordinal);
continue;
}
//Error case
//... etc ...
}
}
}
}
数据库管理器
静态类`DB`是一个轻量级的单例类,用于包含其他类型并提供简单的辅助例程。
初始化
这包装了一个`SqlConnectionStringBuilder`,并包含了企业环境所需的常规设置。
/// <summary>
/// Initialises the specified data source.
/// </summary>
/// <param name="displayName">The display name.</param>
/// <param name="dataSource">The data source.</param>
/// <param name="initialCatalog">The initial catalog.</param>
/// <param name="applicationName">Name of the application.</param>
public static void Initialise(string displayName, string dataSource, string initialCatalog, string applicationName)
{
//See source for details
}
执行
以下通用类型化例程可用,所有例程都支持或不支持`DB.Parameters`和取消功能:
- ExecuteNonQuery
- ExecuteScalar
- ExecuteReader - 返回类型化的`DB.Reader`。
- ExecuteReaderRaw - 返回常规的`SqlDataReader`。
对于非常简单的固定位置的私有存储过程,使用`ExecuteReaderRaw`可能更容易,在这种情况下,以下扩展例程会很有用:
// Extension method: handles the db null translation - additionally, translates empty and whitespace strings to null
internal static string GetOptionalString(this SqlDataReader @this, int index)
{
var res = @this[index];
if (res != DBNull.Value)
{
var str = (string)res;
if (!string.IsNullOrWhiteSpace(str)) return str;
}
return null;
}
// Extension method: handles the db null translation
internal static T? GetOptionalValue<T>(this SqlDataReader @this, int index) where T : struct
{
var res = @this[index];
return res != DBNull.Value ? (T?)res : default(T?);
}
TVP 助手
表值参数只需匹配 SQL UDT 中定义的每个字段的位置和类型。因此,以下辅助例程可以在不增加输入开销的情况下提供类型安全流的好处:
// Creates a table value parameter based on a collection of values
public static Table<KeyStruct<T>> CreateTVP<T>(int count, IEnumerable<T> items)
{
var table = new Table<KeyStruct<T>>(count);
foreach (var item in items)
{
table.Row.Key = item;
table.RowCommitAsNew();
}
return table;
}
代理
示例描述了一种情况,其中可能存在大量`Key`类的实例,并且只需要将该类中的一部分字段流式传输到 TVP 中。在这种情况下,`DB.Table`类可以直接使用`Key`实例,如下所示:
//Used as a surrogate to allow mapping onto the public Key
public struct udtKeySurrogate
{
//Cache the mapping
public readonly static DB.Field[] MD = DB.Table<Key>.CreateMetaData<udtKeySurrogate>();
//Names match Key; order matches Sql Udt
public long Value;
}
public struct uspNVPLoadById
{
//The normal key
[DB.Parameter("@ids")]
public DB.Table<Key> Keys;
//... etc ...
}
在这种独特的情况下,`DB`上的一个助手可以方便地进行数据流式传输。
var keys = new List<Key>(/* lots of keys */);
//Create the TVP data directly off the Keys
//i.e. table will refer to the same Key instances and will not duplicate all the data again
var table = DB.CreateTVP(keys, udtKeySurrogate.MD);
然后继续进行常规设置(有关详细信息,请参阅示例)。
//1. Setup
var args = new DB.Parameters<uspNVPLoadById>("dbo.uspNVPLoadById");
args.Data.Keys = table;
运行示例
有两个示例显示了`AddingTypeSafety`和`DTOsToDataGrid`情况的计时,这些情况不需要数据库。
要运行其他示例,应执行以下操作:
- 使用 Visual Studio 2012 将可以正常工作,否则,可能需要在 2010 年安装数据工具;
- 双击`SQL.publish.xml`文件;
- 如果显示无法连接,则找到`sqllocaldb.exe`并使用`start`和`Projects`作为参数运行它。
SqlLocalDB 管理工具位于MSDN上的 SqlLocalDB.MSI 中。
参考文献
历史
- 1.0 - 初稿
- 1.1 - 添加了关于亚正常和内存效率的部分