在日常开发中,我们经常需要在数据访问层面对实体进行增删改查。主流 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[实体对象]

核心设计思路

  1. 特性标注:通过自定义特性(Attribute)标记实体类与数据库表/字段的映射关系
  2. 元数据缓存:首次扫描实体类型后缓存元数据,避免重复反射
  3. 表达式编译:使用表达式树编译强类型 Getter/Setter 委托,替代反射调用,提升性能
  4. SQL 自动生成:根据元数据自动构建 INSERT/UPDATE/DELETE 语句
  5. 对象映射:从 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; }
}

关键点:GetterSetter 是通过表达式树编译的委托,性能接近直接调用。

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 具有以下特点:

  1. 零依赖:不依赖任何第三方 ORM 框架,完全基于 ADO.NET + 反射 + 表达式树
  2. 高性能:通过表达式编译的 Getter/Setter 委托,性能接近直接调用
  3. 类型安全:编译期检查属性映射,运行期验证元数据正确性
  4. 线程安全:ConcurrentDictionary 缓存,支持多线程并发访问
  5. 可扩展:支持多数据库方言(SqlDatabase 继承 Database),支持事务

适用场景

  • 不想引入重型 ORM 框架的项目
  • 需要精确控制 SQL 生成的场景
  • 数据结构稳定、CRUD 操作为主的业务系统
  • 学习 ORM 内部实现原理

与主流框架对比

特性 本方案 Dapper EF Core
性能
功能完整度
学习曲线
SQL 控制 完全控制 完全控制 自动生成
适用场景 CRUD 为主 全场景 复杂查询

💡 核心价值:理解元数据驱动和表达式编译的设计思想,不仅可以用于构建 ORM,还可以应用于任何需要高性能对象映射的场景,如序列化、数据导入导出等。