EF Core 6如何基于其他表数据自定义模型只读属性值?
问题
为现有ERP系统开发配套软件,无法修改现有数据库。当前使用Dapper查询数据,但原数据库列名混乱导致增改操作繁琐,计划切换到EF Core 6。需要在Document模型中实现仅可查询的SettledStatus属性,该属性值通过关联Payments表的数据计算得出,计算逻辑对应SQL如下:
select case when sum(Payment.SettledAmount) = 0 then 'N' when (Document.Gross - sum(Payment.SettledAmount)) > 0 then 'C' when (Document.Gross - sum(Payment.SettledAmount)) = 0 then 'R' else 'N' end from Payments where Payment.DocumentId = Document.Id group by Payment.DocumentId
由于Documents表中无SettledStatus字段,该属性需跳过增改操作。尝试通过Payments集合的赋值逻辑计算该属性,代码示例如下:
模型代码
public class Document { public int Id { get; set; } public string Number { get; set; } public string SettledStatus { get; set; } public decimal Gross { get; set; } private List<Payments> _payments; public List<Payments> Payments { get { return _payments; } set { _payments = value; if (_payments.Sum(x => x.SettledAmount) == 0) SettledStatus = "N"; else if (Gross - _payments.Sum(x => x.SettledAmount) > 0) SettledStatus = "C"; else if (Gross - _payments.Sum(x => x.SettledAmount) == 0) SettledStatus = "R"; else SettledStatus = "N"; } } } public class Payments { public int Id { get; set; } public string Number { get; set; } public int DocumentId { get; set; } public decimal SettledAmount { get; set; } public Document Document { get; set; } }
上下文配置
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Document>().HasMany(s => s.Payments).WithOne(s => s.Document); }
请问这样的实现方式是否可行?或者EF Core 6是否有更合适的方案?
回答
现有方案的问题
你的实现方式存在明显缺陷,不可行:
- 仅当
Payments集合被直接赋值时才会触发计算,如果是EF Core延迟加载加载的Payments(比如访问document.Payments时才从数据库拉取数据),不会执行setter逻辑,SettledStatus会保持默认值null。 - 如果后续修改
Payments集合中的元素(添加、删除付款记录,或修改某条付款的SettledAmount),不会自动重新计算SettledStatus,导致属性值与实际数据不一致。 - EF Core会默认将
SettledStatus视为数据库列,执行增改操作时会尝试读写该字段,触发数据库报错(因为表中不存在这个字段)。
EF Core 6的更优方案
针对你的需求,推荐以下几种方案:
1. 客户端只读计算属性(最简单)
将SettledStatus改为只读计算属性,实时基于Payments集合和Gross计算,同时配置EF Core忽略该属性,避免增改时读写数据库:
public class Document { public int Id { get; set; } public string Number { get; set; } public decimal Gross { get; set; } // 配置为延迟加载集合(需安装Microsoft.EntityFrameworkCore.Proxies) public virtual ICollection<Payments> Payments { get; set; } = new List<Payments>(); // 只读计算属性,实时计算状态 public string SettledStatus { get { var totalSettled = Payments.Sum(p => p.SettledAmount); if (totalSettled == 0) return "N"; var remaining = Gross - totalSettled; return remaining > 0 ? "C" : "R"; } } }
上下文配置中忽略该属性:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Document>() .HasMany(s => s.Payments) .WithOne(s => s.Document); // 告知EF Core该属性不是数据库字段 modelBuilder.Entity<Document>().Ignore(d => d.SettledStatus); }
注意:使用延迟加载时,访问SettledStatus会触发Payments的数据库查询;若要避免额外查询,可在查询Document时用Include(d => d.Payments)预先加载关联数据。
2. 查询时投影计算(性能更优)
如果不需要在实体类中保留该属性,仅在查询时获取,可直接在LINQ查询中投影计算,避免加载整个Payments集合:
var documents = await dbContext.Documents .Select(d => new { Document = d, TotalSettled = d.Payments.Sum(p => p.SettledAmount), }) .Select(x => new DocumentWithStatus { Id = x.Document.Id, Number = x.Document.Number, Gross = x.Document.Gross, SettledStatus = x.TotalSettled == 0 ? "N" : (x.Document.Gross - x.TotalSettled) > 0 ? "C" : "R" }) .ToListAsync();
配套的DTO类:
public class DocumentWithStatus { public int Id { get; set; } public string Number { get; set; } public decimal Gross { get; set; } public string SettledStatus { get; set; } }
这种方式的优势是:EF Core会将LINQ查询转换为你提供的SQL逻辑,直接在数据库端计算总和,性能远优于客户端加载所有Payments后计算。
3. 映射数据库标量函数(进阶复用)
如果需要在多个查询中复用该计算逻辑,可将SQL逻辑封装为数据库标量函数(无需修改现有表,仅需创建函数),再在EF Core中映射该函数:
首先在数据库中创建函数:
CREATE FUNCTION dbo.GetDocumentSettledStatus(@DocumentId int, @Gross decimal(18,2)) RETURNS char(1) AS BEGIN DECLARE @TotalSettled decimal(18,2) = (SELECT SUM(SettledAmount) FROM Payments WHERE DocumentId = @DocumentId) IF @TotalSettled = 0 RETURN 'N' DECLARE @Remaining decimal(18,2) = @Gross - @TotalSettled RETURN CASE WHEN @Remaining > 0 THEN 'C' ELSE 'R' END END
在EF Core中映射该函数:
public class AppDbContext : DbContext { // 映射数据库函数 [DbFunction("GetDocumentSettledStatus", Schema = "dbo")] public static string GetDocumentSettledStatus(int documentId, decimal gross) { // 仅用于EF Core映射,无需实现 throw new NotImplementedException(); } // ... DbSet和其他配置 }
最后在Document类中使用该函数:
public class Document { public int Id { get; set; } public string Number { get; set; } public decimal Gross { get; set; } public virtual ICollection<Payments> Payments { get; set; } = new List<Payments>(); [NotMapped] // 标记为非数据库字段 public string SettledStatus => AppDbContext.GetDocumentSettledStatus(Id, Gross); }
这种方式可在LINQ查询中直接使用SettledStatus属性,EF Core会自动转换为数据库函数调用,兼顾复用性和性能。
内容的提问来源于stack exchange,提问作者TronComputers

