在日常开发中,我们经常需要在数据访问层面对实体进行增删改查。主流 ORM 框架(如 Entity Framework、Dapper)功能强大,但在某些场景下,我们需要一个更轻量、更可控的方案。本文将基于 SwitchData 项目的实际代码,讲解如何从零构建一个基于反射表达式的微型 ORM,通过 MetadataCache 实现元数据驱动的 CRUD 操作。
一、整体架构设计
整个微型 ORM 由以下几个核心组件构成:
graph TD
A[实体类 Entity] -->|反射扫描| B[MetadataCache<br/>元数据缓存]
B -->|生成| C[TableMetadata<br/>表元数据]
C -->|包含| D[ColumnMetadata[]<br/>字段元数据]
D -->|Expression 编译| E[Getter/Setter 委托]
C -->|驱动| F[SqlDatabase<br/>SQL 生成]
F -->|生成 SQL| G[INSERT/UPDATE/DELETE]
E -->|映射| H[ObjectMapper<br/>对象映射器]
H -->|填充| I[实体对象]
核心设计思路
- 特性标注:通过自定义特性(Attribute)标记实体类与数据库表/字段的映射关系
- 元数据缓存:首次扫描实体类型后缓存元数据,避免重复反射
- 表达式编译:使用表达式树编译强类型 Getter/Setter 委托,替代反射调用,提升性能
- SQL 自动生成:根据元数据自动构建 INSERT/UPDATE/DELETE 语句
- 对象映射:从 IDataReader/DataRow 自动填充实体对象
二、自定义特性标注
2.1 表级别特性
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class DbTableAttribute : Attribute
{
public string Name { get; }
public DbObjectType ObjectType { get; }
public DbTableAttribute(string name, DbObjectType objectType = DbObjectType.Table)
{
Name = name.Trim();
ObjectType = objectType;
}
}
2.2 字段级别特性
[AttributeUsage(AttributeTargets.Property)]
public sealed class DbColumnAttribute : Attribute
{
public string Name { get; }
public bool ReadOnly { get; init; } = false;
public DbColumnAttribute(string name)
{
Name = name.Trim();
}
}
[AttributeUsage(AttributeTargets.Property)]
public sealed class DbPrimaryKeyAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Property)]
public sealed class DbIdentityAttribute : Attribute { }
2.3 实体示例
[DbTable("T_User")]
public class User
{
[DbColumn("ID")]
[DbPrimaryKey]
[DbIdentity]
public int Id { get; set; }
[DbColumn("User_Name")]
public string UserName { get; set; }
[DbColumn("Email")]
public string Email { get; set; }
[DbColumn("Created_At")]
public DateTime CreatedAt { get; set; }
}
三、元数据缓存实现
3.1 ColumnMetadata:字段元数据
public sealed class ColumnMetadata
{
public required string PropertyName { get; init; }
public required string ColumnName { get; init; }
public required Type PropertyType { get; init; }
public required PropertyInfo Property { get; init; }
public required Type DataType { get; init; }
public required DbType DbType { get; init; }
public required bool IsNullable { get; init; }
public required bool IsPrimaryKey { get; init; }
public bool IsIdentity { get; init; }
public bool ReadOnly { get; init; } = false;
public required Func<object, object> Getter { get; init; }
public required Action<object, object> Setter { get; init; }
}
关键点:Getter 和 Setter
是通过表达式树编译的委托,性能接近直接调用。
3.2 TableMetadata:表元数据
public sealed class TableMetadata
{
public required Type Type { get; init; }
public string DbTableName { get; private set; }
public DbObjectType ObjectType { get; private set; }
public required IReadOnlyList<ColumnMetadata> Columns { get; init; }
public required ColumnMetadata PrimaryKey { get; init; }
public required ColumnMetadata IdentityColumn { get; init; }
public required IReadOnlyList<ColumnMetadata> InsertableColumns { get; init; }
public required IReadOnlyList<ColumnMetadata> UpdatableColumns { get; init; }
public required IReadOnlyDictionary<string, ColumnMetadata> PropertyMap { get; init; }
public required IReadOnlyDictionary<string, ColumnMetadata> ColumnMap { get; init; }
}
InsertableColumns 和 UpdatableColumns 是根据字段特性自动筛选的: - Insertable:非只读、非自增 - Updatable:非只读、非主键、非自增
3.3 MetadataCache:线程安全缓存
public static class MetadataCache
{
private static readonly ConcurrentDictionary<Type, TableMetadata> Cache = new();
public static TableMetadata GetTableMetadata<T>()
=> GetTableMetadata(typeof(T));
public static TableMetadata GetTableMetadata(Type type)
{
ArgumentNullException.ThrowIfNull(type);
return Cache.GetOrAdd(type, CreateMetadata);
}
private static TableMetadata CreateMetadata(Type type)
{
// 1. 扫描实体类型的所有属性
// 2. 提取 DbColumnAttribute、DbPrimaryKeyAttribute、DbIdentityAttribute
// 3. 创建 ColumnMetadata 列表(含编译的 Getter/Setter)
// 4. 验证元数据正确性(重复列名、多个主键等)
// 5. 生成 InsertableColumns、UpdatableColumns
// 6. 创建 TableMetadata 并返回
}
}
使用 ConcurrentDictionary.GetOrAdd
实现线程安全的懒加载缓存。
四、表达式树编译 Getter/Setter
这是整个 ORM
的性能关键所在。相比反射调用(property.GetValue(obj)),表达式树编译的委托性能可以提升数倍。
4.1 编译 Getter
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> getter = obj => (object)((User)obj).Id;
4.2 编译 Setter
private static Action<object, object> CreateSetter(PropertyInfo property)
{
// 生成:(object obj, object val) => ((T)obj).Property = (TProperty)val
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();
}
等价于手写代码:
Action<object, object> setter = (obj, val) => ((User)obj).Id = (int)val;
4.3 性能对比
| 方式 | 100万次调用耗时 |
|---|---|
| 直接调用 | ~50ms |
| 反射 Invoke | ~800ms |
| 表达式编译 | ~60ms |
表达式编译的 Getter/Setter 性能接近直接调用,比反射快一个数量级。
五、SQL 自动生成与 CRUD 实现
5.1 Insert 操作
public override async Task<T> InsertAsync<T>(T entity,
DbTransaction transaction = null, CancellationToken cancellationToken = default)
{
CheckEntity<T>(entity, EntityOperation.Insert);
var metadata = MetadataCache.GetTableMetadata<T>();
string columns = string.Join(", ", metadata.InsertableColumns.Select(x => $"[{x.ColumnName}]"));
string values = string.Join(", ", metadata.InsertableColumns.Select(x => $"@{x.PropertyName}"));
// 根据元数据构建 INSERT SQL
string sql = $"INSERT INTO [{metadata.DbTableName}]\n({columns})\nVALUES\n({values})";
// 自增主键:获取 SCOPE_IDENTITY()
if (metadata.IdentityColumn != null)
{
sql += $"\nSELECT CAST(SCOPE_IDENTITY() AS {GetSqlIdentityType(metadata.IdentityColumn.DataType)});";
}
using var command = GetSqlStringCommand(sql);
// 使用编译的 Getter 提取属性值
foreach (var column in metadata.InsertableColumns)
{
object value = column.Getter(entity);
AddInParameter(command, $"@{column.PropertyName}", column.DbType, value);
}
// 执行并回填自增ID
if (metadata.IdentityColumn != null)
{
object result = transaction != null
? await ExecuteScalarAsync(command, transaction, cancellationToken)
: await ExecuteScalarAsync(command, cancellationToken);
ObjectMapper.SetValue(entity, metadata, metadata.IdentityColumn.ColumnName, result);
}
else
{
if (transaction != null)
await ExecuteNonQueryAsync(command, transaction, cancellationToken);
else
await ExecuteNonQueryAsync(command, cancellationToken);
}
return entity;
}
5.2 Update 操作
public virtual async Task<int> UpdateAsync<T>(T entity,
DbTransaction transaction = null, CancellationToken cancellationToken = default)
{
CheckEntity<T>(entity, EntityOperation.Update);
var metadata = MetadataCache.GetTableMetadata<T>();
var primaryKey = metadata.PrimaryKey;
object keyValue = primaryKey.Getter(entity);
string setClause = string.Join(", ",
metadata.UpdatableColumns.Select(x => $"[{x.ColumnName}] = @{x.PropertyName}"));
string whereClause = $"[{primaryKey.ColumnName}] = @{primaryKey.PropertyName}";
string sql = $"UPDATE [{metadata.DbTableName}]\nSET {setClause}\nWHERE {whereClause}";
using var command = GetSqlStringCommand(sql);
foreach (var column in metadata.UpdatableColumns)
{
object value = column.Getter(entity);
AddInParameter(command, $"@{column.PropertyName}", column.DbType, value);
}
AddInParameter(command, $"@{primaryKey.PropertyName}", primaryKey.DbType, keyValue);
int result = transaction != null
? await ExecuteNonQueryAsync(command, transaction, cancellationToken)
: await ExecuteNonQueryAsync(command, cancellationToken);
return result;
}
5.3 Delete 操作
public virtual async Task<int> DeleteAsync<T>(object keyValue,
DbTransaction transaction = null, CancellationToken cancellationToken = default)
{
CheckEntity<T>(keyValue, EntityOperation.Delete);
var metadata = MetadataCache.GetTableMetadata<T>();
var primaryKey = metadata.PrimaryKey;
string sql = $"DELETE FROM [{metadata.DbTableName}]\nWHERE [{primaryKey.ColumnName}] = @{primaryKey.PropertyName}";
using var command = GetSqlStringCommand(sql);
AddInParameter(command, $"@{primaryKey.PropertyName}", primaryKey.DbType, keyValue);
return transaction != null
? await ExecuteNonQueryAsync(command, transaction, cancellationToken)
: await ExecuteNonQueryAsync(command, cancellationToken);
}
六、ObjectMapper:对象映射器
ObjectMapper 负责从 IDataReader 或 DataRow 读取数据并填充实体对象。
public static class ObjectMapper
{
public static T MapFrom<T>(IDataRecord record) where T : class, new()
{
T entity = new();
return SetFrom(entity, record);
}
public static T SetFrom<T>(T entity, IDataRecord record) where T : class, new()
{
var metadata = MetadataCache.GetTableMetadata<T>();
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;
}
public static void SetValue<T>(T entity, TableMetadata metadata,
string columnName, object value) where T : class
{
if (!metadata.ColumnMap.TryGetValue(columnName, out ColumnMetadata column))
return;
if (column.PropertyType == typeof(string))
{
column.Setter(entity, value?.ToString());
return;
}
if (value is null || value == DBNull.Value)
{
if (!column.IsNullable)
throw new InvalidOperationException($"字段 {column.ColumnName} 不允许为空");
column.Setter(entity, null);
return;
}
if (!column.PropertyType.IsInstanceOfType(value))
value = Convert.ChangeType(value, column.PropertyType);
column.Setter(entity, value);
}
}
映射流程
graph LR
A[IDataReader] -->|遍历字段| B[GetColumnName + GetValue]
B --> C{ColumnMap 查找}
C -->|找到| D[类型转换]
D --> E[编译的 Setter 赋值]
C -->|未找到| F[跳过]
E --> G[实体对象]
F --> G
七、实体验证机制
在执行任何操作前,CheckEntity 方法会进行全面的验证:
protected virtual void CheckEntity<T>(object value, EntityOperation operation)
{
var entityName = typeof(T).FullName;
// 实体必须有表名映射
if (string.IsNullOrWhiteSpace(metadata.DbTableName))
throw new InvalidOperationException($"实体 {entityName} 未映射数据库表");
// 实体必须有主键
if (primaryKey == null)
throw new InvalidOperationException($"实体 {entityName} 未定义主键");
// 视图禁止写操作
if (operation != EntityOperation.Select &&
metadata.ObjectType == DbObjectType.View)
throw new InvalidOperationException("视图禁止写操作");
// Insert 需要有可插入字段
if (operation == EntityOperation.Insert &&
metadata.InsertableColumns.Count == 0)
throw new InvalidOperationException("无可插入字段");
// 主键值校验
if (keyValue.IsNullOrEmptyValue() &&
(operation != EntityOperation.Insert || metadata.IdentityColumn == null))
throw new InvalidOperationException("主键值不能为空");
}
八、元数据校验规则
MetadataCache 在创建元数据时会执行以下校验:
// 不允许重复列名
var duplicateColumns = columns
.GroupBy(x => x.ColumnName, StringComparer.OrdinalIgnoreCase)
.Where(x => x.Count() > 1).ToList();
if (duplicateColumns.Count > 0)
throw new InvalidOperationException($"重复的字段名:{duplicateColumns.Select(x => x.Key)}");
// 只能有一个主键
if (keyColumns.Count > 1)
throw new InvalidOperationException("定义了多个主键字段");
// 自增列必须是主键
if (identityColumns.Count == 1 &&
(keyColumns.Count == 0 || keyColumns[0] != identityColumns[0]))
throw new InvalidOperationException("自增字段必须是主键");
九、使用示例
9.1 定义实体
[DbTable("T_APN_Info")]
public class ApnInfo
{
[DbColumn("ID")]
[DbPrimaryKey]
[DbIdentity]
public int Id { get; set; }
[DbColumn("APN_Name")]
public string ApnName { get; set; }
[DbColumn("Status")]
public int Status { get; set; }
[DbColumn("Created_At")]
public DateTime CreatedAt { get; set; }
}
9.2 依赖注入
services.AddSingleton<Database>(sp =>
new SqlDatabase(configuration.GetConnectionString("DefaultConnection")));
9.3 CRUD 操作
public class ApnService
{
private readonly Database _db;
public ApnService(Database db)
{
_db = db;
}
public async Task<ApnInfo> CreateAsync(ApnInfo apn)
{
return await _db.InsertAsync(apn);
}
public async Task<ApnInfo> GetByIdAsync(int id)
{
return await _db.LoadAsync<ApnInfo>(id);
}
public async Task<int> UpdateAsync(ApnInfo apn)
{
return await _db.UpdateAsync(apn);
}
public async Task<int> DeleteAsync(int id)
{
return await _db.DeleteAsync<ApnInfo>(id);
}
}
十、总结
本文实现的微型 ORM 具有以下特点:
- 零依赖:不依赖任何第三方 ORM 框架,完全基于 ADO.NET + 反射 + 表达式树
- 高性能:通过表达式编译的 Getter/Setter 委托,性能接近直接调用
- 类型安全:编译期检查属性映射,运行期验证元数据正确性
- 线程安全:ConcurrentDictionary 缓存,支持多线程并发访问
- 可扩展:支持多数据库方言(SqlDatabase 继承 Database),支持事务
适用场景
- 不想引入重型 ORM 框架的项目
- 需要精确控制 SQL 生成的场景
- 数据结构稳定、CRUD 操作为主的业务系统
- 学习 ORM 内部实现原理
与主流框架对比
| 特性 | 本方案 | Dapper | EF Core |
|---|---|---|---|
| 性能 | 高 | 高 | 中 |
| 功能完整度 | 中 | 中 | 高 |
| 学习曲线 | 低 | 低 | 高 |
| SQL 控制 | 完全控制 | 完全控制 | 自动生成 |
| 适用场景 | CRUD 为主 | 全场景 | 复杂查询 |
💡 核心价值:理解元数据驱动和表达式编译的设计思想,不仅可以用于构建 ORM,还可以应用于任何需要高性能对象映射的场景,如序列化、数据导入导出等。