引言
在中小型 .NET 项目中,引入完整的 EF Core 往往过于沉重,而直接写 ADO.NET 又容易陷入重复的样板代码。SwitchData 项目采用了一种务实的中间路线:手写一个约 2000 行代码的轻量级 ORM 框架,涵盖 ADO.NET 抽象层、元数据反射、表达式树编译、三级缓存、泛型 CRUD 和批量操作,性能接近手写 ADO.NET,开发体验接近 Dapper。
本文将从源码层面拆解这套 ORM 的完整实现,核心亮点有三个:
- Database 抽象基类:Provider 模式 + 模板方法,一套接口支持多数据库
- MetadataCache 元数据驱动:反射扫描特性 + 表达式树编译 Getter/Setter,实现高效属性访问
- 三级缓存机制:MetadataCache + ParameterCache + SchemaCache,平衡性能与内存
整体架构
mermaid graph TD A[业务代码] --> B[DatabaseFactory] B --> C[SqlDatabase] C --> D((MetadataCache<br/>反射+表达式树)) C --> E((ParameterCache<br/>存储过程参数)) C --> F((SchemaCache<br/>表结构)) C --> G[ObjectMapper] D --> H[ColumnMetadata<br/>Getter/Setter委托] G --> H C --> I[DbConnectionManager<br/>连接管理] C --> J[Database 抽象基类<br/>ExecuteScalar/Reader/NonQuery]
核心类关系:Database(抽象基类)→ SqlDatabase(SQL Server 实现)→ 泛型 CRUD 方法;MetadataCache(类型元数据缓存)→ ObjectMapper(对象映射)→ 实体类;DbConnectionManager(多库连接管理)→ DatabaseFactory(工厂入口)。
一、Database 抽象基类:Provider 模式 + 模板方法
Database 是整个 ORM 的骨架类,采用经典的 Provider 模式 和 模板方法模式:
`csharp public abstract class Database { private static readonly ParameterCache parameterCache = new(); private static readonly SchemaCache schemaCache = new();
private readonly DbProviderFactory dbProviderFactory;
private readonly string connectionString;
protected Database(string connectionString, DbProviderFactory dbProviderFactory)
{
this.connectionString = connectionString;
this.dbProviderFactory = dbProviderFactory;
}
} `
关键设计:传入 DbProviderFactory 实现数据库无关性,派生类只需传入具体的 Factory 实例。例如 SQL Server 版本:
csharp public class SqlDatabase : Database { public SqlDatabase(string connectionString) : base(connectionString, SqlClientFactory.Instance) { } }
基类统一封装了 ADO.NET 的四大操作方法 ExecuteScalar、ExecuteNonQuery、ExecuteReader、ExecuteDataTable,以及完整的异步版本。每个方法内部的模式高度统一:
csharp public virtual async Task<object> ExecuteScalarAsync(DbCommand command, CancellationToken ct = default) { await using DbConnection connection = await OpenConnectionAsync(ct); PrepareCommand(command, connection); return await DoExecuteScalarAsync(command, ct); }
同时还定义了需要派生类实现的抽象方法(模板方法):
csharp protected abstract void DeriveParameters(DbCommand discoveryCommand); protected abstract List<string> DoGetPrimaryKeys(string tableName); public abstract void BulkCopy(DataTable dataTable, string targetTableName, ...); public abstract bool TableExists(string tableName);
这样新增 MySQL/PostgreSQL 只需要继承 Database、传入对应的 DbProviderFactory、实现模板方法即可,业务代码完全不用改。
二、MetadataCache:反射扫描 + 表达式树编译
MetadataCache 是整个 ORM 的灵魂,它把”实体类型 → 表结构映射”这件事通过反射和表达式树一次性编译好并缓存。
特性标注
实体类通过特性声明表和字段的映射关系:
`csharp [DbTable(“PcfPolicySubscriptionStatistics”)] public class PcfPolicySubscription { [DbColumn(“Id”)] [DbPrimaryKey] [DbIdentity] public long Id { get; set; }
[DbColumn("PolicyId")]
public string PolicyId { get; set; }
[DbColumn("StrategyId")]
public string StrategyId { get; set; }
[DbColumn("TotalSubscriber")]
public int TotalSubscriber { get; set; }
} `
四种特性各司其职:DbTable 指定表名、DbColumn 指定字段名和只读标记、DbPrimaryKey 标记主键、DbIdentity 标记自增长列。
反射扫描 + 表达式树编译
首次访问某个实体类型时,MetadataCache.GetTableMetadata
`csharp public static TableMetadata GetTableMetadata(Type type) { return Cache.GetOrAdd(type, CreateMetadata); }
private static TableMetadata CreateMetadata(Type type) {
DbTableAttribute dbTableAttribute =
type.GetCustomAttribute
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
DbColumnAttribute columnAttribute = property.GetCustomAttribute<DbColumnAttribute>();
if (columnAttribute == null) continue;
columns.Add(new ColumnMetadata
{
PropertyName = property.Name,
ColumnName = columnAttribute.Name,
PropertyType = property.PropertyType,
DbType = Database.ConvertToDbType(property.PropertyType),
IsNullable = property.PropertyType.IsNullable(),
IsPrimaryKey = property.IsDefined(typeof(DbPrimaryKeyAttribute)),
IsIdentity = property.IsDefined(typeof(DbIdentityAttribute)),
Getter = CreateGetter(property),
Setter = CreateSetter(property)
});
}
// 校验唯一性、主键、自增长约束
return metadata;
} `
重点在 CreateGetter 和 CreateSetter:使用 表达式树编译 出高性能的委托,避免每次调用都走反射。
表达式树编译 Getter
csharp private static Func<object, object> CreateGetter(PropertyInfo property) { // (object obj) => (object) ((T)obj).Property ParameterExpression instance = Expression.Parameter(typeof(object), "obj"); UnaryExpression cast = Expression.Convert(instance, property.DeclaringType); MemberExpression propertyAccess = Expression.Property(cast, property); UnaryExpression box = Expression.Convert(propertyAccess, typeof(object)); return Expression.Lambda<Func<object, object>>(box, instance).Compile(); }
编译后的 Func<object, object> 委托调用开销接近直接属性访问(约 10ns),比反射 PropertyInfo.GetValue(约 50ns)快 5 倍以上。
表达式树编译 Setter
`csharp private static Action<object, object> CreateSetter(PropertyInfo property) { if (!property.CanWrite || IsInitOnly(property)) return (, ) => { };
ParameterExpression instance = Expression.Parameter(typeof(object), "obj");
ParameterExpression value = Expression.Parameter(typeof(object), "value");
UnaryExpression instanceCast = Expression.Convert(instance, property.DeclaringType);
MemberExpression propertyAccess = Expression.Property(instanceCast, property);
UnaryExpression valueCast = Expression.Convert(value, property.PropertyType);
BinaryExpression assign = Expression.Assign(propertyAccess, valueCast);
return Expression.Lambda<Action<object, object>>(assign, instance, value).Compile();
} `
注意对 C# 9 init-only 属性做了检测,返回空委托避免运行时报错。
约束校验
CreateMetadata 里还有一系列编译期约束检查,在启动时就发现配置错误:
`csharp var duplicateColumns = columns.GroupBy(x => x.ColumnName, StringComparer.OrdinalIgnoreCase) .Where(x => x.Count() > 1).ToList(); if (duplicateColumns.Count > 0) throw …;
if (keyColumns.Count > 1) throw …; if (identityColumns.Count > 1) throw …; if (identityColumns.Count == 1 && keyColumns[0] != identityColumns[0]) throw …; `
三、ObjectMapper:从 DataReader/DataRow 到实体
ObjectMapper 把 ADO.NET 的 IDataRecord 或 DataRow 映射为实体对象,核心是从 MetadataCache 取 TableMetadata,再通过 ColumnMap 字典按列名匹配属性:
`csharp public static T SetFrom
private static void SetValue
if (column.PropertyType == typeof(string))
{
column.Setter(entity, value.ToStringValue());
return;
}
if (value is null || value == DBNull.Value)
{
if (!column.IsNullable) throw new InvalidOperationException(...);
column.Setter(entity, null);
return;
}
if (!column.PropertyType.IsInstanceOfType(value))
value = value.ConvertTo(column.PropertyType);
column.Setter(entity, value);
} `
设计亮点:O(1) 字典查找、自动处理 DBNull/可空类型、类型自动转换、支持 IDbObject 回调接口。
四、SqlDatabase 泛型 CRUD
SqlDatabase 在继承基类的基础上,新增了完整的泛型 CRUD 方法,以 InsertAsync 为例:
`csharp public override async Task
string columns = string.Join(", ", metadata.InsertableColumns.Select(x => $"[{x.ColumnName}]"));
string values = string.Join(", ", metadata.InsertableColumns.Select(x => $"{BuildParameterName(x.PropertyName)}"));
string sql = "INSERT INTO [{metadata.DbTableName}] ({columns}) VALUES ({values})";
if (metadata.IdentityColumn != null)
{
sql += $"SELECT CAST(SCOPE_IDENTITY() AS {GetSqlIdentityType(metadata.IdentityColumn.DataType)});";
}
using var command = GetSqlStringCommand(sql);
foreach (var column in metadata.InsertableColumns)
{
object value = column.Getter(entity);
AddInParameter(command, BuildParameterName(column.PropertyName), column.DbType, value);
}
if (metadata.IdentityColumn != null)
{
object result = await ExecuteScalarAsync(command, cancellationToken);
ObjectMapper.SetValue(entity, metadata, metadata.IdentityColumn.ColumnName, result);
}
else
{
await ExecuteNonQueryAsync(command, cancellationToken);
}
return entity;
} `
核心步骤:从元数据动态构建 SQL → 表达式树 Getter 取属性 → 参数化查询执行 → SCOPE_IDENTITY 回填自增长列。整个过程没有手写一条 SQL 插入语句,也没有反射访问属性。
五、三级缓存机制
第一级:MetadataCache(类型元数据缓存)
csharp private static readonly ConcurrentDictionary<Type, TableMetadata> Cache = new();
键是实体类型,值是预编译好的 TableMetadata。应用生命周期内只扫描一次类型,后续全部命中内存缓存。
第二级:ParameterCache(存储过程参数缓存)
csharp private readonly ConcurrentDictionary<CacheKey, IDataParameter[]> paramCache = new();
使用 (ConnectionString, CommandText) 作为键,首次执行存储过程时通过 SqlCommandBuilder.DeriveParameters 从数据库发现参数结构,之后直接复用。避免了每次调用都去数据库查询存储过程签名。
第三级:SchemaCache(表结构缓存)
csharp private readonly ConcurrentDictionary<CacheKey, DataTable[]> schemaCache = new();
缓存 FillSchema 返回的表结构和 GetPrimaryKeys 查询结果。返回时会 Clone 一份 DataTable/List,因为 DataTable 是引用类型,外部修改会破坏缓存。
缓存清理
csharp public static void ClearParameterCache() => parameterCache.Clear(); public static void ClearSchemaCache() => schemaCache.Clear();
六、DbConnectionManager:多库连接 + 加密 + 重试
`csharp public static class DbConnectionManager { private static readonly ConcurrentDictionary<string, string> _connectionStrings = new();
public static void Init(IConfiguration configuration)
{
if (Interlocked.Exchange(ref _initialized, 1) == 1) return;
var section = configuration.GetSection("ConnectionStrings");
foreach (var child in section.GetChildren())
{
_connectionStrings[child.Key] = SymmetricCryptographer.Decrypt(child.Value);
}
}
public static string Get(string databaseName) => _connectionStrings[databaseName];
} `
- Interlocked.Exchange 确保 Init 只执行一次(线程安全的单例初始化)
- 连接字符串通过 SymmetricCryptographer.Decrypt 解密
- 内置 WaitForReadyAsync 重试机制,服务启动时等待数据库就绪
七、DatabaseFactory:工厂入口
csharp public static class DatabaseFactory { public static Database CreateDatabase(string databaseName) => new SqlDatabase(DbConnectionManager.Get(databaseName)); }
业务代码只需一行:ar db = DatabaseFactory.CreateDatabase(“MainDb”);。
总结
这套轻量级 ORM 的设计哲学是用最少的代码覆盖最高频的场景:
| 组件 | 职责 | 关键技术 |
|---|---|---|
| Database | ADO.NET 抽象层 | Provider 模式 + 模板方法 |
| MetadataCache | 实体元数据缓存 | 反射扫描 + 表达式树编译 |
| ObjectMapper | 对象映射 | 列名匹配 + 类型转换 |
| ParameterCache | 存储过程参数缓存 | ConcurrentDictionary + ICloneable |
| SchemaCache | 表结构缓存 | ConcurrentDictionary + DataTable.Clone |
| DbConnectionManager | 多库连接管理 | 加密解密 + Interlocked 防重入 |
| SqlDatabase | SQL Server 泛型 CRUD | SCOPE_IDENTITY 回填 + BulkCopy |
相比 EF Core 约 50MB 的依赖体积和数秒的启动时反射开销,这套 ORM 只有 2000 行源码、零外部依赖(仅依赖 Microsoft.Data.SqlClient)、首次反射后全部命中缓存。如果你的项目不需要 LINQ-to-SQL 和复杂的变更追踪,这种手写 ORM + 表达式树 + 三级缓存的方案可能是最合适的选择。