引言

在中小型 .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() 通过 ConcurrentDictionary.GetOrAdd 触发反射扫描:

`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(T entity, IDataRecord record) where T : class, new() { TableMetadata metadata = MetadataCache.GetTableMetadata(); for (int i = 0; i < record.FieldCount; i++) { string columnName = record.GetName(i); object value = record.GetValue(i); SetValue(entity, metadata, columnName, value); } if (entity is IDbObject callback) callback.OnLoaded(); return entity; }

private static void SetValue(T entity, TableMetadata metadata, string columnName, object value) { if (!metadata.ColumnMap.TryGetValue(columnName, out ColumnMetadata column)) return;

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 InsertAsync(T entity, DbTransaction transaction = null, CancellationToken cancellationToken = default) { var metadata = MetadataCache.GetTableMetadata();

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 + 表达式树 + 三级缓存的方案可能是最合适的选择。