C#泛型类中如何正确访问泛型类型T的成员属性?
Blazor + EF Core Code First + OData 泛型控制器复用问题修复
问题描述
开发过程中需要实现OData控制器逻辑复用,编写泛型基类控制器时遇到两个泛型类型访问相关的编译错误:
- 初始版本仅给泛型参数添加
class约束时,查询表达式中无法访问泛型类型的ID属性,抛出CS1061编译错误 - 给泛型参数添加自定义
IFileData接口约束后,ID属性访问错误消失,但调用EF Core的_context.Set<T>()方法时抛出CS0452编译错误
临时通过反射获取ID属性值的方案可以运行,但写法冗余、无编译期检查、存在额外性能开销,不够优雅。
初始错误代码
public class DS2DbContext : DbContext { public DbSet<DocumentFileData> DocumentFileData { get; set; } public DbSet<WatermarkFileData> WatermarkFileData { get; set; } public DS2DbContext(DbContextOptions<DS2DbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } } public class FileDataController<T> : ODataController where T : class { private readonly DS2DbContext _context; private readonly ILogger<FileDataController<T>> _logger; public FileDataController(DS2DbContext dbContext, ILogger<FileDataController<T>> logger) { _logger = logger; _context = dbContext; } [EnableQuery] public SingleResult<T> Get([FromODataUri] Guid ID) { // 编译错误CS1061: 'T'不包含'ID'的定义 var result = _context.Set<T>().Where(i => i.ID == ID); return SingleResult.Create(result); } }
添加接口约束后的错误代码
public class FileDataController<T> : ODataController where T : IFileData { private readonly DS2DbContext _context; private readonly ILogger<FileDataController<T>> _logger; public FileDataController(DS2DbContext dbContext, ILogger<FileDataController<T>> logger) { _logger = logger; _context = dbContext; } [EnableQuery] public SingleResult<T> Get([FromODataUri] Guid ID) { var result = _context.Set<T>().Where(i => i.ID == ID); // 编译错误CS0452: 类型'T'必须是引用类型才能作为DbSet<TEntity>的泛型参数 return SingleResult.Create(result); } }
临时反射方案
var result = _context.Set<T>() .Where(x => (Guid) x.GetType().GetProperty("ID").GetValue(x) == ID);
业务背景
需求文档、水印两类文件数据结构完全一致,需要按语义拆分存储到独立数据库表。EF Core Code First模式下需要两个独立实体类才能生成对应表,两类数据的OData接口业务逻辑完全一致,已经通过公共接口+基类实现了实体层代码复用,需要实现控制器层复用,避免后续逻辑变更时同步修改多份重复代码。
已实现的实体层代码:
public interface IFileData { public Guid ID { get; set; } } using System.ComponentModel.DataAnnotations; public class FileData : IFileData { [Key] public Guid ID { get; set; } = Guid.Empty; } public class DocumentFileData : FileData { } public class WatermarkFileData : FileData { }
修复方案
泛型参数需要同时添加引用类型约束和自定义接口约束,即可同时满足EF Core对DbSet泛型参数的要求,以及编译器对泛型成员访问的检查要求,无需使用反射。
修正后的泛型基类控制器
public class FileDataController<T> : ODataController where T : class, IFileData { private readonly DS2DbContext _context; private readonly ILogger<FileDataController<T>> _logger; public FileDataController(DS2DbContext dbContext, ILogger<FileDataController<T>> logger) { _logger = logger; _context = dbContext; } [EnableQuery] public SingleResult<T> Get([FromODataUri] Guid ID) { // 无编译错误,支持编译期成员检查,无额外性能开销 var result = _context.Set<T>().Where(i => i.ID == ID); return SingleResult.Create(result); } // 其他公共CRUD逻辑都可以写在基类中,直接访问IFileData定义的属性即可 }
具体业务控制器实现
泛型基类编写完成后,对应两个实体的控制器只需要继承基类,不需要重复编写业务逻辑:
public class DocumentFileDataController : FileDataController<DocumentFileData> { public DocumentFileDataController(DS2DbContext dbContext, ILogger<FileDataController<DocumentFileData>> logger) : base(dbContext, logger) { } } public class WatermarkFileDataController : FileDataController<WatermarkFileData> { public WatermarkFileDataController(DS2DbContext dbContext, ILogger<FileDataController<WatermarkFileData>> logger) : base(dbContext, logger) { } }
该方案下所有公共业务逻辑只需要在泛型基类中维护一次,OData可以正常识别两个派生控制器的路由,后续新增同结构文件类型只需要新增对应实体类和空的派生控制器即可。
内容的提问来源于stack exchange,提问作者Razzupaltuff
相关产品推荐
相关产品推荐

