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 无法更新具有关系的实体