无外键约束时能否使用导航属性?EF Core排序逻辑实现问询
无外键关联场景下的排序Func实现方案
问题背景
现有系统通过Func<IQueryable<T>, IOrderedQueryable<T>>定义排序规则,示例代码如下:
Func<IQueryable<T>, IOrderedQueryable<T>> sort = q => q.OrderBy(r => r.Arrival);
排序调用逻辑:
var railcars = DbContext.Railcars .Where(r => r.ArrivalDate != null) .OrderBy(sort) .ToList();
当前需要实现关联无外键的Transfers表的排序逻辑,但无法直接引用DbContext,且系统重构成本极高。目标是找到无需引用DbContext的实现方案,或模拟导航属性的用法。
实体结构说明:Transfers表的FromId/ToId为逻辑外键,需结合FromType/ToType确定关联表(如TransferType.Railcar关联Railcars表):
public enum TransferType { Storage = 0, Railcar = 1, Truck = 2, Ship = 3, } public class Transfer { public int Id { get; set; } public TransferType FromType { get; set; } public int FromId { get; set; } public TransferType ToType { get; set; } public int ToId { get; set; } public DateTime TimeStamp { get; set; } public double Quantity { get; set; } } public class Railcar { public int Id { get; set; } public string RailcarNumber { get; set; } public DateTime? Arrival { get; set; } public DateTime? Departure { get; set; } }
解决方案
方案1:配置无外键的导航属性(推荐)
EF Core支持在无数据库外键约束的情况下,通过Fluent API手动配置导航属性,实现类似外键关联的用法,无需在排序Func中直接引用DbContext。
步骤1:添加导航属性到实体
修改Railcar类,新增对应转出/转入记录的导航属性:
public class Railcar { public int Id { get; set; } public string RailcarNumber { get; set; } public DateTime? Arrival { get; set; } public DateTime? Departure { get; set; } // 从该Railcar转出的记录 public ICollection<Transfer> OutgoingTransfers { get; set; } // 转入该Railcar的记录 public ICollection<Transfer> IncomingTransfers { get; set; } }
步骤2:在DbContext中配置导航属性
在DbContext的OnModelCreating方法中,通过Fluent API配置关联规则,包含逻辑外键的过滤条件:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置转出记录关联 modelBuilder.Entity<Railcar>() .HasMany(r => r.OutgoingTransfers) .WithOne() // Transfer无反向导航属性,使用WithOne() .HasForeignKey(t => t.FromId) .HasPrincipalKey(r => r.Id) .IsRequired(false) .HasFilter($"FromType = {(int)TransferType.Railcar}"); // 过滤Railcar类型的转出记录 // 配置转入记录关联 modelBuilder.Entity<Railcar>() .HasMany(r => r.IncomingTransfers) .WithOne() .HasForeignKey(t => t.ToId) .HasPrincipalKey(r => r.Id) .IsRequired(false) .HasFilter($"ToType = {(int)TransferType.Railcar}"); // 过滤Railcar类型的转入记录 }
步骤3:编写排序Func
现在可以直接使用导航属性编写排序逻辑,完全符合原有系统的使用方式:
Func<IQueryable<Railcar>, IOrderedQueryable<Railcar>> sort = q => q.OrderBy(r => r.InboundQuantity - r.OutgoingTransfers.Sum(t => t.Quantity) + r.IncomingTransfers.Sum(t => t.Quantity));
方案2:使用LINQ子查询(无需修改实体配置)
如果无法修改实体或DbContext配置,可通过LINQ子查询实现,利用EF Core的查询翻译能力,无需直接引用DbContext实例。
排序Func示例:
Func<IQueryable<Railcar>, IOrderedQueryable<Railcar>> sort = q => q.OrderBy(r => r.InboundQuantity - (from t in q.Provider.CreateQuery<Transfer>( Expression.Call( typeof(EntityFrameworkQueryableExtensions), nameof(EntityFrameworkQueryableExtensions.AsQueryable), new[] { typeof(Transfer) }, Expression.Constant(((IInfrastructure<IServiceProvider>)q.Provider.GetService(typeof(ICurrentDbContext))).Instance.Set<Transfer>()) )) where t.FromId == r.Id && t.FromType == TransferType.Railcar select t.Quantity).Sum() + (from t in q.Provider.CreateQuery<Transfer>( Expression.Call( typeof(EntityFrameworkQueryableExtensions), nameof(EntityFrameworkQueryableExtensions.AsQueryable), new[] { typeof(Transfer) }, Expression.Constant(((IInfrastructure<IServiceProvider>)q.Provider.GetService(typeof(ICurrentDbContext))).Instance.Set<Transfer>()) )) where t.ToId == r.Id && t.ToType == TransferType.Railcar select t.Quantity).Sum());
该方案通过IQueryable的Provider获取当前上下文的Transfers集合,避免直接引用DbContext实例,但代码复杂度较高,仅作为无法使用方案1时的替代。
内容的提问来源于stack exchange,提问作者Jonathan Wood
相关产品推荐
相关产品推荐

