C#泛型接口实现疑问:如何通过接口返回不同数据库实体类型
首先可以明确地说:将接口定义为泛型接口ISqlTradeDataRetriever<T>,然后让实现类指定具体实体类型(比如SqlCommodityDataRetriever : ISqlTradeDataRetriever<Commodity>)的方案是完全可行的,而且这是比你最初写法更合理、类型更安全的方向。
为什么你最初的写法会报错?
你最初的接口是非泛型的,但方法是泛型的,这种写法会让编译器无法在编译期确认Commodity类型可以安全转换为泛型参数T——哪怕你在代码里做了typeof(T) == typeof(Commodity)的判断,编译器也不会为这种运行时检查做类型推断,所以会抛出转换错误。虽然你可以通过强制类型转换(比如return (Task<T>)(object)_context.Commodities.FirstOrDefaultAsync(...))绕开编译错误,但这种写法非常不安全,容易引发运行时类型转换异常,而且代码可读性极差,完全不推荐。
泛型接口的正确实现示例
先定义带约束的泛型接口(添加where T : class约束可以让EF的DbSet操作更顺畅):
public interface ISqlTradeDataRetriever<T> where T : class { Task<T> GetByPrimaryKeysAsync(string symbol, string date); Task<ICollection<T>> GetAllAsync(string symbol); Task<ICollection<T>> GetDataInDateRange(string symbol, KeyValuePair<DateTime, DateTime> dateRange); }
然后实现针对Commodity的检索类:
public class SqlCommodityDataRetriever : ISqlTradeDataRetriever<Commodity> { private readonly IBatlGroupWebContext _context; public SqlCommodityDataRetriever(IBatlGroupWebContext context) { _context = context; } public async Task<Commodity> GetByPrimaryKeysAsync(string symbol, string date) { var tradeDate = Convert.ToDateTime(date); return await _context.Commodities.FirstOrDefaultAsync(m => m.Symbol == symbol && m.TradeDate == tradeDate); } public async Task<ICollection<Commodity>> GetAllAsync(string symbol) { return await _context.Commodities.Where(m => m.Symbol == symbol).ToListAsync(); } public async Task<ICollection<Commodity>> GetDataInDateRange(string symbol, KeyValuePair<DateTime, DateTime> dateRange) { return await _context.Commodities .Where(m => m.Symbol == symbol && m.TradeDate >= dateRange.Key && m.TradeDate <= dateRange.Value) .ToListAsync(); } }
这种写法不仅没有语法错误,而且类型安全,代码逻辑清晰,每个实现类只负责对应实体的检索逻辑。
进一步减少代码冗余的优化方案
如果你的4个实体都有Symbol和TradeDate这类通用属性,我们可以通过抽象基类+统一实体接口的方式,把通用逻辑抽离出来,彻底消除重复代码:
- 先定义一个所有交易数据实体都要实现的接口,统一通用属性:
public interface ITradeDataEntity { string Symbol { get; set; } DateTime TradeDate { get; set; } }
让你的Commodity和其他3个实体都实现这个接口:
public class Commodity : ITradeDataEntity { public string Symbol { get; set; } public DateTime TradeDate { get; set; } // 其他实体特有属性 }
- 创建抽象基类,实现通用的检索逻辑:
public abstract class BaseSqlTradeDataRetriever<T> : ISqlTradeDataRetriever<T> where T : class, ITradeDataEntity { protected readonly IBatlGroupWebContext _context; protected readonly DbSet<T> _dbSet; protected BaseSqlTradeDataRetriever(IBatlGroupWebContext context) { _context = context; _dbSet = context.Set<T>(); // 利用EF的Set<T>方法获取对应DbSet } public async Task<T> GetByPrimaryKeysAsync(string symbol, string date) { var tradeDate = Convert.ToDateTime(date); return await _dbSet.FirstOrDefaultAsync(m => m.Symbol == symbol && m.TradeDate == tradeDate); } public async Task<ICollection<T>> GetAllAsync(string symbol) { return await _dbSet.Where(m => m.Symbol == symbol).ToListAsync(); } public async Task<ICollection<T>> GetDataInDateRange(string symbol, KeyValuePair<DateTime, DateTime> dateRange) { return await _dbSet .Where(m => m.Symbol == symbol && m.TradeDate >= dateRange.Key && m.TradeDate <= dateRange.Value) .ToListAsync(); } }
- 每个实体的检索类只需要继承基类即可,不需要重复写通用逻辑:
public class SqlCommodityDataRetriever : BaseSqlTradeDataRetriever<Commodity> { public SqlCommodityDataRetriever(IBatlGroupWebContext context) : base(context) { } // 如果Commodity有特有的检索需求,再在这里添加或重写方法即可 }
这样其他3个实体的检索类也可以用同样的方式实现,代码量会大幅减少,后期维护也更方便。
内容的提问来源于stack exchange,提问作者dinotom

