Entity Framework Core 2.1 无法更新具有关系的实体

Posted

技术标签:

【中文标题】Entity Framework Core 2.1 无法更新具有关系的实体【英文标题】:Entity Framework Core 2.1 failling to update entities with relations 【发布时间】:2018-12-10 17:59:00 【问题描述】:

我目前在使用 EF Core 2.1 和本机客户端用于更新包含多个级别的嵌入对象的对象时遇到问题。 我已经阅读了这两个主题:

Entity Framework Core: Fail to update Entity with nested value objects

https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities

我从中了解到,目前在 EF Core 2 中更新对象确实不是那么明显。但我还没有设法找到可行的解决方案。 每次尝试时,我都会遇到一个异常,告诉我 EF 已经跟踪了一个“步骤”。

我的模型如下所示:

//CIApplication the root class I’m trying to update
public class CIApplication : ConfigurationItem // -> derive of BaseEntity which holds the ID and some other properties  


    //Collection of DeploymentScenario
    public virtual ICollection<DeploymentScenario> DeploymentScenarios  get; set; 

    //Collection of SoftwareMeteringRules
    public virtual ICollection<SoftwareMeteringRule> SoftwareMeteringRules  get; set; 

//与Application有一对多关系的部署场景。一个部署场景包含两个步骤列表

public class DeploymentScenario : BaseEntity


    //Collection of substeps
    public virtual ICollection<Step> InstallSteps  get; set; 
    public virtual ICollection<Step> UninstallSteps  get; set; 

    //Navigation properties Parent CI
    public Guid? ParentCIID  get; set; 
    public virtual CIApplication ParentCI  get; set; 

//Step,也比较复杂,也是自引用的

public class Step : BaseEntity


    public string ScriptBlock  get; set; 


    //Parent Step Navigation property
    public Guid? ParentStepID  get; set; 
    public virtual Step ParentStep  get; set; 

    //Parent InstallDeploymentScenario Navigation property
    public Guid? ParentInstallDeploymentScenarioID  get; set; 
    public virtual DeploymentScenario ParentInstallDeploymentScenario  get; set; 

    //Parent InstallDeploymentScenario Navigation property
    public Guid? ParentUninstallDeploymentScenarioID  get; set; 
    public virtual DeploymentScenario ParentUninstallDeploymentScenario  get; set; 

    //Collection of sub steps
    public virtual ICollection<Step> SubSteps  get; set; 

    //Collection of input variables
    public virtual List<ScriptVariable> InputVariables  get; set; 
    //Collection of output variables
    public virtual List<ScriptVariable> OutPutVariables  get; set; 


这是我的更新方法,我知道它很丑,不应该在控制器中,但我每两个小时更改一次,因为如果在网上找到解决方案,我会尝试实施。 所以这是最后一次迭代来自 https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities

public async Task<IActionResult> PutCIApplication([FromRoute] Guid id, [FromBody] CIApplication cIApplication)
    
        _logger.LogWarning("Updating CIApplication " + cIApplication.Name);

        if (!ModelState.IsValid)
        
            return BadRequest(ModelState);
        

        if (id != cIApplication.ID)
        
            return BadRequest();
        

        var cIApplicationInDB = _context.CIApplications
            .Include(c => c.Translations)
            .Include(c => c.DeploymentScenarios).ThenInclude(d => d.InstallSteps).ThenInclude(s => s.SubSteps)
            .Include(c => c.DeploymentScenarios).ThenInclude(d => d.UninstallSteps).ThenInclude(s => s.SubSteps)
            .Include(c => c.SoftwareMeteringRules)
            .Include(c => c.Catalogs)
            .Include(c => c.Categories)
            .Include(c => c.OwnerCompany)
            .SingleOrDefault(c => c.ID == id);

        _context.Entry(cIApplicationInDB).CurrentValues.SetValues(cIApplication);

        foreach(var ds in cIApplication.DeploymentScenarios)
        
            var existingDeploymentScenario = cIApplicationInDB.DeploymentScenarios.FirstOrDefault(d => d.ID == ds.ID);

            if (existingDeploymentScenario == null)
            
                cIApplicationInDB.DeploymentScenarios.Add(ds);
            
            else
            
                _context.Entry(existingDeploymentScenario).CurrentValues.SetValues(ds);

                foreach(var step in existingDeploymentScenario.InstallSteps)
                
                    var existingStep = existingDeploymentScenario.InstallSteps.FirstOrDefault(s => s.ID == step.ID);

                    if (existingStep == null)
                    
                        existingDeploymentScenario.InstallSteps.Add(step);
                    
                    else
                    
                        _context.Entry(existingStep).CurrentValues.SetValues(step);
                    
                
            
        
        foreach(var ds in cIApplicationInDB.DeploymentScenarios)
        
            if(!cIApplication.DeploymentScenarios.Any(d => d.ID == ds.ID))
            
                _context.Remove(ds);
            
        

        //_context.Update(cIApplication);
        try
        
            await _context.SaveChangesAsync();
        
        catch (DbUpdateConcurrencyException e)
        
            if (!CIApplicationExists(id))
            
                return NotFound();
            
            else
            
                throw;
            
        
        catch(Exception e)
        
        

        return Ok(cIApplication);
    

到目前为止,我遇到了这个异常: 无法跟踪实体类型“Step”的实例,因为已在跟踪另一个具有键值“ID: e29b3c1c-2e06-4c7b-b0cd-f8f1c5ccb7b6”的实例。

我注意到客户端之前没有进行任何“get”操作,即使是这种情况,我也已将 AsNoTracking 放在我的 get 方法中。 客户端在更新之前所做的唯一操作是“_context.CIApplications.Any(e => e.ID == id);”来检查我是否应该添加新记录或更新现有记录。

几天以来我一直在与这个问题作斗争,所以如果有人能帮助我朝着正确的方向前进,我将不胜感激。 非常感谢

更新:

我在控制器中添加了以下代码:

var existingStep = existingDeploymentScenario.InstallSteps.FirstOrDefault(s => s.ID == step.ID);
                    entries = _context.ChangeTracker.Entries();
                    if (existingStep == null)
                    
                        existingDeploymentScenario.InstallSteps.Add(step);
                        entries = _context.ChangeTracker.Entries();
                    

条目 = _context.ChangeTracker.Entries();在添加包含新步骤的新部署场景后,行会立即引发“步骤已跟踪”异常。

就在它之前,新的部署场景和步骤不在跟踪器中,我已经在数据库中检查了它们的 ID 没有重复。

我还检查了我的 Post 方法,现在它也失败了……我将它恢复为默认方法,里面没有花哨的东西:

[HttpPost]
    public async Task<IActionResult> PostCIApplication([FromBody] CIApplication cIApplication)
    
        if (!ModelState.IsValid)
        
            return BadRequest(ModelState);
        
        var entries = _context.ChangeTracker.Entries();
        _context.CIApplications.Add(cIApplication);
        entries = _context.ChangeTracker.Entries();
        await _context.SaveChangesAsync();
        entries = _context.ChangeTracker.Entries();
        return CreatedAtAction("GetCIApplication", new  id = cIApplication.ID , cIApplication);
    

Entries 开头为空且 _context.CIApplications.Add(cIApplication);行仍在引发关于部署场景中唯一一步的异常...

所以当我尝试在我的上下文中添加内容时显然有问题,但现在我感觉完全迷失了。它可以帮助我如何在启动时声明我的上下文:

services.AddDbContext<MyAppContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
            b => b.MigrationsAssembly("DeployFactoryDataModel")),
            ServiceLifetime.Transient
            );

添加我的上下文类:

public class MyAppContext : DbContext

    private readonly IHttpContextAccessor _contextAccessor;
    public MyAppContext(DbContextOptions<MyAppContext> options, IHttpContextAccessor contextAccessor) : base(options)
    
        _contextAccessor = contextAccessor;
    


    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    

        optionsBuilder.EnableSensitiveDataLogging();
    

    public DbSet<Step> Steps  get; set; 
    //public DbSet<Sequence> Sequences  get; set; 
    public DbSet<DeploymentScenario> DeploymentScenarios  get; set; 
    public DbSet<ConfigurationItem> ConfigurationItems  get; set; 
    public DbSet<CIApplication> CIApplications  get; set; 
    public DbSet<SoftwareMeteringRule> SoftwareMeteringRules  get; set; 
    public DbSet<Category> Categories  get; set; 
    public DbSet<ConfigurationItemCategory> ConfigurationItemsCategories  get; set; 
    public DbSet<Company> Companies  get; set; 
    public DbSet<User> Users  get; set; 
    public DbSet<Group> Groups  get; set; 
    public DbSet<Catalog> Catalogs  get; set; 
    public DbSet<CIDriver> CIDrivers  get; set; 
    public DbSet<DriverCompatiblityEntry> DriverCompatiblityEntries  get; set; 
    public DbSet<ScriptVariable> ScriptVariables  get; set; 

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    
        //Step one to many with step for sub steps
        modelBuilder.Entity<Step>().HasMany(s => s.SubSteps).WithOne(s => s.ParentStep).HasForeignKey(s => s.ParentStepID);

        //Step one to many with step for variables
        modelBuilder.Entity<Step>().HasMany(s => s.InputVariables).WithOne(s => s.ParentInputStep).HasForeignKey(s => s.ParentInputStepID);
        modelBuilder.Entity<Step>().HasMany(s => s.OutPutVariables).WithOne(s => s.ParentOutputStep).HasForeignKey(s => s.ParentOutputStepID);

        //Step one to many with sequence
        //modelBuilder.Entity<Step>().HasOne(step => step.ParentSequence).WithMany(seq => seq.Steps).HasForeignKey(step => step.ParentSequenceID).OnDelete(DeleteBehavior.Cascade);

        //DeploymentScenario One to many with install steps
        modelBuilder.Entity<DeploymentScenario>().HasMany(d => d.InstallSteps).WithOne(s => s.ParentInstallDeploymentScenario).HasForeignKey(s => s.ParentInstallDeploymentScenarioID);

        //DeploymentScenario One to many with uninstall steps
        modelBuilder.Entity<DeploymentScenario>().HasMany(d => d.UninstallSteps).WithOne(s => s.ParentUninstallDeploymentScenario).HasForeignKey(s => s.ParentUninstallDeploymentScenarioID);

        //DeploymentScenario one to one with sequences
        //modelBuilder.Entity<DeploymentScenario>().HasOne(ds => ds.InstallSequence).WithOne(seq => seq.IDeploymentScenario).HasForeignKey<DeploymentScenario>(ds => ds.InstallSequenceID).OnDelete(DeleteBehavior.Cascade);
        //modelBuilder.Entity<DeploymentScenario>().HasOne(ds => ds.UninstallSequence).WithOne(seq => seq.UDeploymentScenario).HasForeignKey<DeploymentScenario>(ds => ds.UninstallSequenceID);

        //Step MUI config
        modelBuilder.Entity<Step>().Ignore(s => s.Description);
        modelBuilder.Entity<Step>().HasMany(s => s.Translations).WithOne().HasForeignKey(x => x.StepTranslationId);

        //Sequence MUI config
        //modelBuilder.Entity<Sequence>().Ignore(s => s.Description);
        //modelBuilder.Entity<Sequence>().HasMany(s => s.Translations).WithOne().HasForeignKey(x => x.SequenceTranslationId);

        //DeploymentScenario MUI config
        modelBuilder.Entity<DeploymentScenario>().Ignore(s => s.Name);
        modelBuilder.Entity<DeploymentScenario>().Ignore(s => s.Description);
        modelBuilder.Entity<DeploymentScenario>().HasMany(s => s.Translations).WithOne().HasForeignKey(x => x.DeploymentScenarioTranslationId);

        //CIApplication  relations
        //CIApplication one to many relation with Deployment Scenario
        modelBuilder.Entity<CIApplication>().HasMany(ci => ci.DeploymentScenarios).WithOne(d => d.ParentCI).HasForeignKey(d => d.ParentCIID).OnDelete(DeleteBehavior.Cascade);
        modelBuilder.Entity<CIApplication>().HasMany(ci => ci.SoftwareMeteringRules).WithOne(d => d.ParentCI).HasForeignKey(d => d.ParentCIID).OnDelete(DeleteBehavior.Cascade);

        // CIDriver relations
        // CIAPpplication one to many relation with DriverCompatibilityEntry
        modelBuilder.Entity<CIDriver>().HasMany(ci => ci.CompatibilityList).WithOne(c => c.ParentCI).HasForeignKey(c => c.ParentCIID).OnDelete(DeleteBehavior.Restrict);

        //ConfigurationItem MUI config
        modelBuilder.Entity<ConfigurationItem>().Ignore(s => s.Name);
        modelBuilder.Entity<ConfigurationItem>().Ignore(s => s.Description);
        modelBuilder.Entity<ConfigurationItem>().HasMany(s => s.Translations).WithOne().HasForeignKey(x => x.ConfigurationItemTranslationId);

        //category MUI config
        modelBuilder.Entity<Category>().Ignore(s => s.Name);
        modelBuilder.Entity<Category>().Ignore(s => s.Description);
        modelBuilder.Entity<Category>().HasMany(s => s.Translations).WithOne().HasForeignKey(x => x.CategoryTranslationId);

        //CI Categories Many to Many
        modelBuilder.Entity<ConfigurationItemCategory>().HasKey(cc => new  cc.CategoryId, cc.CIId );
        modelBuilder.Entity<ConfigurationItemCategory>().HasOne(cc => cc.Category).WithMany(cat => cat.ConfigurationItems).HasForeignKey(cc => cc.CategoryId);
        modelBuilder.Entity<ConfigurationItemCategory>().HasOne(cc => cc.ConfigurationItem).WithMany(ci => ci.Categories).HasForeignKey(cc => cc.CIId);

        //CI Catalog Many to Many
        modelBuilder.Entity<CICatalog>().HasKey(cc => new  cc.CatalogId, cc.ConfigurationItemId );
        modelBuilder.Entity<CICatalog>().HasOne(cc => cc.Catalog).WithMany(cat => cat.CIs).HasForeignKey(cc => cc.CatalogId);
        modelBuilder.Entity<CICatalog>().HasOne(cc => cc.ConfigurationItem).WithMany(ci => ci.Catalogs).HasForeignKey(cc => cc.ConfigurationItemId);

        //Company Customers Many to Many
        modelBuilder.Entity<CompanyCustomers>().HasKey(cc => new  cc.CustomerId, cc.ProviderId );
        modelBuilder.Entity<CompanyCustomers>().HasOne(cc => cc.Provider).WithMany(p => p.Customers).HasForeignKey(cc => cc.ProviderId).OnDelete(DeleteBehavior.Restrict);
        modelBuilder.Entity<CompanyCustomers>().HasOne(cc => cc.Customer).WithMany(c => c.Providers).HasForeignKey(cc => cc.CustomerId);

        //Company Catalog Many to Many
        modelBuilder.Entity<CompanyCatalog>().HasKey(cc => new  cc.CatalogId, cc.CompanyId );
        modelBuilder.Entity<CompanyCatalog>().HasOne(cc => cc.Catalog).WithMany(c => c.Companies).HasForeignKey(cc => cc.CatalogId);
        modelBuilder.Entity<CompanyCatalog>().HasOne(cc => cc.Company).WithMany(c => c.Catalogs).HasForeignKey(cc => cc.CompanyId);

        //Author Catalog Many to Many
        modelBuilder.Entity<CatalogAuthors>().HasKey(ca => new  ca.AuthorId, ca.CatalogId );
        modelBuilder.Entity<CatalogAuthors>().HasOne(ca => ca.Catalog).WithMany(c => c.Authors).HasForeignKey(ca => ca.CatalogId);
        modelBuilder.Entity<CatalogAuthors>().HasOne(ca => ca.Author).WithMany(a => a.AuthoringCatalogs).HasForeignKey(ca => ca.AuthorId);

        //Company one to many with owned Catalog
        modelBuilder.Entity<Company>().HasMany(c => c.OwnedCatalogs).WithOne(c => c.OwnerCompany).HasForeignKey(c => c.OwnerCompanyID).OnDelete(DeleteBehavior.Restrict);
        //Company one to many with owned Categories
        modelBuilder.Entity<Company>().HasMany(c => c.OwnedCategories).WithOne(c => c.OwnerCompany).HasForeignKey(c => c.OwnerCompanyID).OnDelete(DeleteBehavior.Restrict);
        //Company one to many with owned CIs
        modelBuilder.Entity<Company>().HasMany(c => c.OwnedCIs).WithOne(c => c.OwnerCompany).HasForeignKey(c => c.OwnerCompanyID).OnDelete(DeleteBehavior.Restrict);

        //CIDriver one to many with DriverCompatibilityEntry
        modelBuilder.Entity<CIDriver>().HasMany(c => c.CompatibilityList).WithOne(c => c.ParentCI).HasForeignKey(c => c.ParentCIID).OnDelete(DeleteBehavior.Restrict);

        //User Group Many to Many
        modelBuilder.Entity<UserGroup>().HasKey(ug => new  ug.UserId, ug.GroupId );
        modelBuilder.Entity<UserGroup>().HasOne(cg => cg.User).WithMany(ci => ci.Groups).HasForeignKey(cg => cg.UserId);
        modelBuilder.Entity<UserGroup>().HasOne(cg => cg.Group).WithMany(ci => ci.Users).HasForeignKey(cg => cg.GroupId);

        //User one to many with Company
        modelBuilder.Entity<Company>().HasMany(c => c.Employees).WithOne(u => u.Employer).HasForeignKey(u => u.EmployerID).OnDelete(DeleteBehavior.Restrict);
    

更新 2

这是一个指向最小复制示例的单驱动器链接。我还没有在客户端实现 PUT,因为 post 方法已经重现了这个问题。

https://1drv.ms/u/s!AsO87EeN0Fnsk7dDRY3CJeeLT-4Vag

【问题讨论】:

我不知道数据是什么样的,但是这一步可能被选为其他步骤的substep 吗?我注意到您不搜索/更新substeps 我尝试更新的实际应用程序包含一个部署场景,在 installStep 列表中只有一个步骤。更新后的应用程序有一个新的 DeploymentScenario 和一个新步骤。到目前为止,没有任何步骤有子步骤,我希望它首先与一个级别一起工作:)。为了确保测试清晰,我首先重新启动 Web 应用程序,并且在触发控制器的更新操作之前不进行任何选择或操作。 如果没有 MCVE,将很难为您提供帮助。最好的办法是您准备一个可用于重现问题的小型 repo。 Post 方法中的代码非常简单,所以它必须与cIApplication 对象的内容相关——导航属性、PK 和 FK 值等。 cIApplication 下的对象图中似乎有多个“相同”Step 的实例,这很容易发生在双向自引用中。 哦,忘了问,因为它应该很明显。我猜_context 每次都是一个新实例? 【参考方案1】:

您在此处枚举现有步骤,并在现有步骤集合中搜索没有意义的现有步骤。

 foreach(var step in existingDeploymentScenario.InstallSteps)
     var existingStep = existingDeploymentScenario.InstallSteps
         .FirstOrDefault(s => s.ID == step.ID);

虽然它应该是:

foreach(var step in ds.InstallSteps)

【讨论】:

对!我更正了这一点,因此 cIApplicationInDB 在流程结束时与 cIApplication 匹配。但我仍然遇到错误,指出已在跟踪某个步骤。 一个有趣的事实(或不是)是导致问题的步骤是新部署场景中的新步骤。所以它甚至不在数据库中。 哈哈对。当项目添加到上下文中时,我会尝试跟踪 - 是什么行导致了这种情况。当您的step 被添加时,也许可以观看 _context.ChangeTracker.Entries。 我不知道如何解释,但如果我添加: foreach (var dbEntityEntry in _context.ChangeTracker.Entries()) var state = dbEntityEntry.State;在一系列 foreach 之后和尝试之前,当 _context.ChangeTracker.Entries() 被调试器高亮时,我得到“已经跟踪”错误。 检查你Entries。它是否有 2 个相同的steps?如果是这样,请使用watch 跟踪_context.ChangeTracker.Entries。从一开始就在您的代码中逐行执行,观察此集合何时会收到您的重复 step 实体。您需要知道代码中添加重复步骤的行。【参考方案2】:

我想通了,我感到很惭愧。

感谢大家,我终于怀疑是客户和它处理数据的人造成了这个问题。

事实证明,当客户端创建部署场景时,它会创建一个步骤并将其分配给 installStep 和 uninstallSteps 列表,从而导致问题...

我非常确定卸载步骤列表没有被使用,我什至在调试时都没有注意到它。

【讨论】:

以上是关于Entity Framework Core 2.1 无法更新具有关系的实体的主要内容,如果未能解决你的问题,请参考以下文章

Entity Framework core 2.1 多对多选择查询

使用 Entity Framework Core (2.1) 调用标量函数的最佳实践

Entity Framework Core 2.1 无法更新具有关系的实体

Entity Framework Core 2.1 - 拥有类型和嵌套值对象

Entity Framework Core快速开始

Entity Framework Core 快速开始