You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core:如何通过反射或KeyAttribute泛型获取实体主键值

批量实现Lookup实体主键映射(无需逐个修改策略类)

背景与需求

当前系统维护200余个数据库Lookup实体,每个实体包含带[Key]标记的主键、Code(命名规则多为[实体名]Code)和Description字段。原策略类仅返回Code和Description,现需新增主键映射到LookupBase的Id属性,但需避免逐个修改200+策略类。

现有代码示例

Lookup实体示例

public class FruitLookup
{
    [Key]
    public int FruitId { get; set; }

    public string FruitCode { get; set; }

    public string Description { get; set; }
}

原有策略类与基类

策略类示例:

public class FruitLookupStrategy : LookupStrategy
{
   public IQueryable<LookupBase> GetLookupData()
   {
        return this.GetLookupData<FruitLookup>()
            .Select(_ => new LookupBase 
            { 
                Code = _.FruitCode, 
                Description = _.Description 
            })
            .OrderBy(_ => _.Code)
            .AsQueryable();
   }
}

LookupBase类(已新增Id属性):

public class LookupBase
{
    public int Id { get; set; } // 新增主键映射属性
    public string Code { get; set; }
    public string Description { get; set; }
}

基类LookupStrategy的查询方法:

public IList<T> GetLookupData<T>()
{
    return this.context.Set<T>().AsNoTracking().ToList();
}

需要避免的逐个修改方式

public class FruitLookupStrategy : LookupStrategy
{
   public IQueryable<LookupBase> GetLookupData()
   {
        return this.GetLookupData<FruitLookup>()
            .Select(_ => new LookupBase
            { 
                Code = _.FruitCode, 
                Description = _.Description, 
                Id = _.FruitId 
            })
            .OrderBy(_ => _.Code)
            .AsQueryable();
   }
}

解决方案

方法1:泛型基类+EF元数据统一处理

改造基类为泛型抽象类,利用EF Core元数据自动识别主键、Code和Description字段,统一实现映射逻辑:

public abstract class LookupStrategy<TEntity> : LookupStrategy 
    where TEntity : class
{
    public override IQueryable<LookupBase> GetLookupData()
    {
        var dbSet = context.Set<TEntity>().AsNoTracking();
        
        // 通过EF元数据获取主键属性
        var pkProperty = context.Model.FindEntityType(typeof(TEntity))
            .FindPrimaryKey().Properties.First();
        
        // 匹配Code属性(优先按实体名前缀, fallback到"Code"字段)
        var entityName = typeof(TEntity).Name.Replace("Lookup", "");
        var codeProperty = typeof(TEntity).GetProperty($"{entityName}Code") 
                           ?? typeof(TEntity).GetProperty("Code");
        
        var descriptionProperty = typeof(TEntity).GetProperty("Description");

        // 构建表达式树生成Select映射逻辑(EF可转换为SQL,避免客户端评估)
        var parameter = Expression.Parameter(typeof(TEntity), "x");
        var idExpr = Expression.Property(parameter, pkProperty.Name);
        var codeExpr = Expression.Property(parameter, codeProperty.Name);
        var descExpr = Expression.Property(parameter, descriptionProperty.Name);

        var initExpr = Expression.MemberInit(
            Expression.New(typeof(LookupBase)),
            Expression.Bind(typeof(LookupBase).GetProperty(nameof(LookupBase.Id)), idExpr),
            Expression.Bind(typeof(LookupBase).GetProperty(nameof(LookupBase.Code)), codeExpr),
            Expression.Bind(typeof(LookupBase).GetProperty(nameof(LookupBase.Description)), descExpr)
        );

        var selectLambda = Expression.Lambda<Func<TEntity, LookupBase>>(initExpr, parameter);

        return dbSet.Select(selectLambda)
                    .OrderBy(x => x.Code)
                    .AsQueryable();
    }
}

改造后策略类(无需重写方法):

public class FruitLookupStrategy : LookupStrategy<FruitLookup>
{
    // 无额外代码,泛型基类已实现统一逻辑
}

方法2:扩展方法+反射/表达式树

若无法修改基类结构,可创建通用扩展方法统一处理映射:

public static class LookupExtensions
{
    public static IQueryable<LookupBase> ToLookupBase<TEntity>(this IQueryable<TEntity> query, DbContext context) 
        where TEntity : class
    {
        // 获取主键属性
        var pkProperty = context.Model.FindEntityType(typeof(TEntity))
            .FindPrimaryKey().Properties.First();
        
        // 匹配Code属性
        var entityName = typeof(TEntity).Name.Replace("Lookup", "");
        var codeProperty = typeof(TEntity).GetProperty($"{entityName}Code") 
                           ?? typeof(TEntity).GetProperty("Code");
        var descriptionProperty = typeof(TEntity).GetProperty("Description");

        // 构建表达式树
        var parameter = Expression.Parameter(typeof(TEntity), "x");
        var idExpr = Expression.Property(parameter, pkProperty.Name);
        var codeExpr = Expression.Property(parameter, codeProperty.Name);
        var descExpr = Expression.Property(parameter, descriptionProperty.Name);

        var initExpr = Expression.MemberInit(
            Expression.New(typeof(LookupBase)),
            Expression.Bind(typeof(LookupBase).GetProperty(nameof(LookupBase.Id)), idExpr),
            Expression.Bind(typeof(LookupBase).GetProperty(nameof(LookupBase.Code)), codeExpr),
            Expression.Bind(typeof(LookupBase).GetProperty(nameof(LookupBase.Description)), descExpr)
        );

        var selectLambda = Expression.Lambda<Func<TEntity, LookupBase>>(initExpr, parameter);
        return query.Select(selectLambda).OrderBy(x => x.Code);
    }
}

改造后策略类:

public class FruitLookupStrategy : LookupStrategy
{
   public IQueryable<LookupBase> GetLookupData()
   {
        return this.GetLookupData<FruitLookup>()
                   .AsQueryable()
                   .ToLookupBase(this.context);
   }
}

优化建议

  • 若Code属性命名无统一规则,可自定义[LookupCode]特性标记属性,通过反射查找特性替代命名匹配;
  • 若主键类型不唯一(如int/string),可将LookupBase.Id改为object类型,或创建泛型LookupBase<TId>支持不同主键类型;
  • 表达式树方式可确保EF将查询转换为SQL执行,避免客户端内存过滤,保障性能。

内容的提问来源于stack exchange,提问作者Donald N. Mafa

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 19:20:49