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

6
我目前遇到了EF Core 2.1和Web API的问题,该API被本地客户端用于更新包含多层嵌套对象的对象。我已经阅读了以下两个主题:

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

https://learn.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; }
}

//部署方案与应用程序之间存在一对多的关系。部署方案包含两个步骤列表。

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

//步骤,它也很复杂,也是自我引用的

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://learn.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。客户端在更新之前唯一执行的操作是“_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();
                    }

在添加包含新步骤的新部署方案之后,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);
    }

在开始时,条目为空,_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

这里是一个OneDrive链接,包含一个最简化的复现示例。我在客户端中没有实现PUT方法,因为POST方法已经能够复现这个问题。

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


1
如果没有最小可复现示例,帮助您可能会很困难。最好的方法是准备一个小型存储库,可用于重现问题。Post方法中的代码非常简单,因此必须与“cIApplication”对象的内容相关 - 导航属性、PK和FK值等。 - Ivan Stoev
cIApplication对象图下似乎存在多个“相同”的Step实例,这很容易发生双向自引用的情况。 - Gert Arnold
哦,忘了问因为这应该是显而易见的。我猜_context每次都是一个新实例? - Gert Arnold
@IvanStoev 我已经更新了问题,并提供了一个MCVE链接。 - mickael ponsot
@Gert Arnold,是的,这个上下文是新的,但是关于你的第一个评论,我认为客户端创建整个对象的方式有问题。我要深入研究一下。 - mickael ponsot
显示剩余2条评论
2个回答

3
你正在枚举现有的步骤,并在现有步骤集合中搜索不存在的步骤,这是没有意义的。
 foreach(var step in existingDeploymentScenario.InstallSteps)
     var existingStep = existingDeploymentScenario.InstallSteps
         .FirstOrDefault(s => s.ID == step.ID);

应该改为:

foreach(var step in ds.InstallSteps)

哈哈,好的。我会尝试跟踪项目添加到上下文的时间-找出导致这个问题的那一行代码。也许可以观察 step 被添加时 _context.ChangeTracker.Entries 的情况。 - Alex Buyny
我不知道如何解释这个,但是如果我在一系列foreach之后,在try之前添加以下代码:foreach(var dbEntityEntry in _context.ChangeTracker.Entries()) { var state = dbEntityEntry.State; } 当调试器突出显示_context.ChangeTracker.Entries()时,我会收到“已跟踪”错误。 - mickael ponsot
检查您的Entries。它是否有2个相同的steps?如果是这样,请使用watch跟踪_context.ChangeTracker.Entries。从开头逐行查看您的代码,观察此集合何时接收您的重复step实体。您需要知道添加重复步骤的代码行。 - Alex Buyny
我按照您的要求在控制器中几乎每个地方都添加了“entries = _context.ChangeTracker.Entries();”。它可以正常工作,直到我到达“cIApplicationInDB.DeploymentScenarios.Add(upToDateDS)”这一行,它会添加新的部署方案。从这里开始,即使是“entries = _context.ChangeTracker.Entries();”也会引发异常。步骤“已跟踪”从未在跟踪器中显示过,并且该行引发了我看不到的异常。奇怪的是,我的post方法现在也在这样做......所有添加操作都失败了。 - mickael ponsot
你是不是不小心重复添加了同一个条目?而不是更新现有的值为新值? - jaredlee.exe
显示剩余3条评论

1
我想通了,感到很惭愧。
多亏了你们所有人,我终于怀疑客户端及其处理数据的方式导致了这个问题。
结果发现,当客户端创建部署方案时,它会创建一个步骤,并将其分配给installStep和uninstallSteps列表,从而导致了这个问题...
我非常确定uninstallStep列表没有被使用,所以在调试时甚至没有看它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接