【问题标题】:Child entity not saved when added to aggregate root添加到聚合根时未保存子实体
【发布时间】:2019-06-28 11:04:26
【问题描述】:

当我保存更改时,一切看起来都很好。 CaseWorkNote 实体已正确创建并添加到 workNotes 集合(Case 实体的属性)。 当 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();
        }
    }

应用服务类:

    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());
        }
    }

谢谢

【问题讨论】:

  • 尝试将其保存在普通的 ApplicationService(不是 AsyncCrudAppService)中,以便您查看 AsyncCrudAppService 中是否存在问题

标签: c# entity-framework-core aspnetboilerplate


【解决方案1】:

问题是由默认的EF Core属性访问模式和ToList()在这里调用引起的

public IEnumerable<CaseWorkNote> WorkNotes => _workNotes?.ToList();

不确定您遵循哪种类型的方法,但您违反了简单的良好设计规则,即属性(尤其是集合类型)不应在每次获取时分配。不仅因为它效率低,而且还允许像 EF Core 这样的“智能”客户端将实际类型检测为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 成员应该用初始化器或在类构造器中初始化,并且属性 getter 应该直接返回它。如果这个想法是为了防止调用者通过转换结果来访问私有成员,那么还有其他方法可以防止不克隆集合内容的方法。例如:

//Aggregated entity
private readonly HashSet<CaseWorkNote> _workNotes = new HashSet<CaseWorkNote>();
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes.Select(e => e);
//

无论您是否保留导航属性的当前实现,您必须让 EF Core 直接使用支持字段。

【讨论】:

  • 哇,好详细的答案。我非常感谢您花时间解决我的问题。回答您的问题(“您遵循什么类型的方法”)。我遵循了this site 的示例。是的,你的提示解决了这个问题。谢谢。
【解决方案2】:

添加外键属性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
            };
        }
    }

【讨论】:

    猜你喜欢
    • 2012-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多