添加到聚合根时未保存子实体
Child entity not saved when added to aggregate root
当我保存更改时,一切看起来都很好。 CaseWorkNote 实体已正确创建并添加到 workNotes 集合(案例实体的 属性)。
当 CurrentUnitOfWork 调用 DbContext->SaveChanges() 时,我看到我的实体在那里,状态为已添加。
最后什么都没有保存到数据库中。
我的代码中遗漏了什么或做错了什么?
下面是我的代码和跟踪实体的截图。
型号:
public class Case : FullAuditedAggregateRoot<Guid>
{
[Required]
public CaseType Type { get; set; }
[Required]
public string Subject { get; set; }
public string Descripion { get; set; }
//Aggregated entity
private HashSet<CaseWorkNote> _workNotes;
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes?.ToList();
//
public CaseWorkNote AddNote(string text)
{
if (_workNotes is null)
{
_workNotes = new HashSet<CaseWorkNote>();
}
CaseWorkNote workNote = CaseWorkNote.Create(this, text);
_workNotes.Add(workNote);
return workNote;
}
}
public class CaseWorkNote : FullAuditedEntity
{
[ForeignKey("CaseId")]
[Required]
public Case Case { get; private set; }
[Required]
public string Text { get; set; }
private CaseWorkNote() : base() { }
public static CaseWorkNote Create(Case kase, string text)
{
return new CaseWorkNote()
{
Case = kase,
Text = text
};
}
}
数据库上下文:
public class testDbContext : AbpZeroDbContext<Tenant, Role, User, testDbContext>
{
public DbSet<Case> Cases { get; set; }
public DbSet<CaseWorkNote> CaseWorkNotes { get; set; }
public testDbContext(DbContextOptions<testDbContext> options)
: base(options) { }
public override int SaveChanges()
{
//Here I see CaseWorkNote entity with state = "Added"
var entries = this.ChangeTracker.Entries();
foreach (var item in entries)
{
Debug.WriteLine("State: {0}, Type: {1}", item.State.ToString(), item.Entity.GetType().FullName);
}
return base.SaveChanges();
}
}
应用服务Class:
public class CaseAppService : AsyncCrudAppService<Case, CaseDto, Guid, PagedCaseResultRequestDto, CreateCaseDto, UpdateCaseDto>, ICaseAppService
{
//Removed for brevity
...
//
public async Task AddWorkNote(CreateUpdateCaseWorkNoteDto input)
{
var kase = await this.GetEntityByIdAsync(input.CaseId);
kase.AddNote(input.Text);
CurrentUnitOfWork.SaveChanges();
}
protected override async Task<Case> GetEntityByIdAsync(Guid id)
{
var kase = await Repository
.GetAllIncluding(c => c.WorkNotes)
.FirstOrDefaultAsync(c => c.Id == id);
if (kase == null)
{
throw new EntityNotFoundException(typeof(Case), id);
}
return kase;
}
public async Task<ListResultDto<CaseWorkNoteDto>> GetWorkNotes(EntityDto<Guid> entity)
{
var kase = await this.GetEntityByIdAsync(entity.Id);
return new ListResultDto<CaseWorkNoteDto>(MapToEntityDto(kase).WorkNotes.ToList());
}
}
谢谢
添加外键属性CaseId
。
还添加了 Virtual
关键字。
public class CaseWorkNote : FullAuditedEntity
{
[ForeignKey("CaseId")]
[Required]
public virtual Case Case { get; private set; }
public virtual Guid CaseId { get; private set; } /* Added */
[Required]
public virtual string Text { get; set; }
private CaseWorkNote() : base() { }
public static CaseWorkNote Create(Case kase, string text)
{
return new CaseWorkNote()
{
Case = kase,
Text = text
};
}
}
问题是默认的EF Core 属性访问方式和ToList()
调用这里
引起的
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes?.ToList();
不确定您遵循的是哪种方法,但您违反了 属性(尤其是集合类型)不应在每次获取时分配的简单良好设计规则。不仅因为它效率低下,而且还允许像 EF Core 这样的 "smart" 客户端将实际类型检测为 List
,并在 loading related data 时尝试使用它来添加项目。
实际上,对于这种类型的实施,他们正在添加到一个被丢弃的列表中,换句话说,没有地方。因此 EF Core 加载相关数据/导航 属性 修复不起作用,这也可能影响更改跟踪器并导致奇怪的行为。
要解决 EF Core 问题,您应该将 EF Core 配置为直接使用 backing field。最简单的方法是在 OnModelCreating
覆盖中全局设置它:
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field);
它也可以按实体或按实体设置 属性,但我会建议以上内容,而且 EF Core 3.0 中的预期变化之一是 Backing fields will be used by default.
反正现在问题就解决了
不过,遵循良好做法会更好。 _workNotes
成员应该使用初始化器或在 class 构造函数中初始化,属性 getter 应该直接 return 。如果想法是阻止调用者通过强制转换结果来访问私有成员,那么还有其他方法可以阻止不克隆集合内容的方法。例如:
//Aggregated entity
private readonly HashSet<CaseWorkNote> _workNotes = new HashSet<CaseWorkNote>();
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes.Select(e => e);
//
无论您是否保留当前的导航实现 属性,您必须 让 EF Core 直接使用支持字段。
当我保存更改时,一切看起来都很好。 CaseWorkNote 实体已正确创建并添加到 workNotes 集合(案例实体的 属性)。 当 CurrentUnitOfWork 调用 DbContext->SaveChanges() 时,我看到我的实体在那里,状态为已添加。
最后什么都没有保存到数据库中。
我的代码中遗漏了什么或做错了什么?
下面是我的代码和跟踪实体的截图。
型号:
public class Case : FullAuditedAggregateRoot<Guid>
{
[Required]
public CaseType Type { get; set; }
[Required]
public string Subject { get; set; }
public string Descripion { get; set; }
//Aggregated entity
private HashSet<CaseWorkNote> _workNotes;
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes?.ToList();
//
public CaseWorkNote AddNote(string text)
{
if (_workNotes is null)
{
_workNotes = new HashSet<CaseWorkNote>();
}
CaseWorkNote workNote = CaseWorkNote.Create(this, text);
_workNotes.Add(workNote);
return workNote;
}
}
public class CaseWorkNote : FullAuditedEntity
{
[ForeignKey("CaseId")]
[Required]
public Case Case { get; private set; }
[Required]
public string Text { get; set; }
private CaseWorkNote() : base() { }
public static CaseWorkNote Create(Case kase, string text)
{
return new CaseWorkNote()
{
Case = kase,
Text = text
};
}
}
数据库上下文:
public class testDbContext : AbpZeroDbContext<Tenant, Role, User, testDbContext>
{
public DbSet<Case> Cases { get; set; }
public DbSet<CaseWorkNote> CaseWorkNotes { get; set; }
public testDbContext(DbContextOptions<testDbContext> options)
: base(options) { }
public override int SaveChanges()
{
//Here I see CaseWorkNote entity with state = "Added"
var entries = this.ChangeTracker.Entries();
foreach (var item in entries)
{
Debug.WriteLine("State: {0}, Type: {1}", item.State.ToString(), item.Entity.GetType().FullName);
}
return base.SaveChanges();
}
}
应用服务Class:
public class CaseAppService : AsyncCrudAppService<Case, CaseDto, Guid, PagedCaseResultRequestDto, CreateCaseDto, UpdateCaseDto>, ICaseAppService
{
//Removed for brevity
...
//
public async Task AddWorkNote(CreateUpdateCaseWorkNoteDto input)
{
var kase = await this.GetEntityByIdAsync(input.CaseId);
kase.AddNote(input.Text);
CurrentUnitOfWork.SaveChanges();
}
protected override async Task<Case> GetEntityByIdAsync(Guid id)
{
var kase = await Repository
.GetAllIncluding(c => c.WorkNotes)
.FirstOrDefaultAsync(c => c.Id == id);
if (kase == null)
{
throw new EntityNotFoundException(typeof(Case), id);
}
return kase;
}
public async Task<ListResultDto<CaseWorkNoteDto>> GetWorkNotes(EntityDto<Guid> entity)
{
var kase = await this.GetEntityByIdAsync(entity.Id);
return new ListResultDto<CaseWorkNoteDto>(MapToEntityDto(kase).WorkNotes.ToList());
}
}
谢谢
添加外键属性CaseId
。
还添加了 Virtual
关键字。
public class CaseWorkNote : FullAuditedEntity
{
[ForeignKey("CaseId")]
[Required]
public virtual Case Case { get; private set; }
public virtual Guid CaseId { get; private set; } /* Added */
[Required]
public virtual string Text { get; set; }
private CaseWorkNote() : base() { }
public static CaseWorkNote Create(Case kase, string text)
{
return new CaseWorkNote()
{
Case = kase,
Text = text
};
}
}
问题是默认的EF Core 属性访问方式和ToList()
调用这里
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes?.ToList();
不确定您遵循的是哪种方法,但您违反了 属性(尤其是集合类型)不应在每次获取时分配的简单良好设计规则。不仅因为它效率低下,而且还允许像 EF Core 这样的 "smart" 客户端将实际类型检测为 List
,并在 loading related data 时尝试使用它来添加项目。
实际上,对于这种类型的实施,他们正在添加到一个被丢弃的列表中,换句话说,没有地方。因此 EF Core 加载相关数据/导航 属性 修复不起作用,这也可能影响更改跟踪器并导致奇怪的行为。
要解决 EF Core 问题,您应该将 EF Core 配置为直接使用 backing field。最简单的方法是在 OnModelCreating
覆盖中全局设置它:
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field);
它也可以按实体或按实体设置 属性,但我会建议以上内容,而且 EF Core 3.0 中的预期变化之一是 Backing fields will be used by default.
反正现在问题就解决了
不过,遵循良好做法会更好。 _workNotes
成员应该使用初始化器或在 class 构造函数中初始化,属性 getter 应该直接 return 。如果想法是阻止调用者通过强制转换结果来访问私有成员,那么还有其他方法可以阻止不克隆集合内容的方法。例如:
//Aggregated entity
private readonly HashSet<CaseWorkNote> _workNotes = new HashSet<CaseWorkNote>();
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes.Select(e => e);
//
无论您是否保留当前的导航实现 属性,您必须 让 EF Core 直接使用支持字段。