Ste*_*ord 6 c# entity-framework entity-framework-core .net-core
根据其他问题(此处和此处),可以通过捕获抛出的异常并检查它的InnerException.
当DbContext.SaveChanges()使用一组重复的数据调用时,会抛出一个异常,但它是一个相当标准的InvalidOperationException,它InnerException是null.
如何检测Entity Framework Core 中的重复键违规?
更新更多上下文(双关语)
我试图捕获/检测的特定违规是在通过多对多关系连接的两个实体(团队和用户)之间添加链接时。
System.InvalidOperationException:无法跟踪实体类型“TeamUser”的实例,因为已在跟踪具有相同键的此类型的另一个实例。添加新实体时,对于大多数键类型,如果未设置键(即,如果键属性为其类型分配了默认值),将创建一个唯一的临时键值。如果您为新实体显式设置键值,请确保它们不会与现有实体或为其他新实体生成的临时值发生冲突。附加现有实体时,请确保只有一个具有给定键值的实体实例附加到上下文。
用户实体类:
public class User
{
[Key]
public string Name { get; set; }
public ICollection<TeamUser> TeamUsers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
团队实体类:
public class Team
{
[Key]
public string Id { get; set; }
[Required]
public string Name { get; set; }
public ICollection<Template> Templates { get; set; }
public ICollection<Checklist> Checklists { get; set; }
public ICollection<TeamUser> TeamUsers { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
TeamUser 实体类:
public class TeamUser
{
public string TeamId { get; set; }
public Team Team { get; set; }
public string UserName { get; set; }
public User User { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
我的DbContext子类配置了 Teams 和 Users 之间的多对多关系:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var teamUserEntity = modelBuilder.Entity<TeamUser>();
teamUserEntity
.HasKey(tu => new { tu.TeamId, tu.UserName });
teamUserEntity
.HasOne(tu => tu.Team)
.WithMany(t => t.TeamUsers)
.HasForeignKey(tu => tu.TeamId);
teamUserEntity
.HasOne(tu => tu.User)
.WithMany(u => u.TeamUsers)
.HasForeignKey(tu => tu.UserName);
}
Run Code Online (Sandbox Code Playgroud)
EF Core 生成的TeamUser表如下:
CREATE TABLE "TeamUser" (
"TeamId" TEXT NOT NULL,
"UserName" TEXT NOT NULL,
CONSTRAINT "PK_TeamUser" PRIMARY KEY ("TeamId", "UserName"),
CONSTRAINT "FK_TeamUser_Teams_TeamId" FOREIGN KEY ("TeamId") REFERENCES "Teams" ("Id") ON DELETE CASCADE,
CONSTRAINT "FK_TeamUser_Users_UserName" FOREIGN KEY ("UserName") REFERENCES "Users" ("Name") ON DELETE CASCADE
);
CREATE INDEX "IX_TeamUser_UserName" ON "TeamUser" ("UserName");
Run Code Online (Sandbox Code Playgroud)
为了处理这样的情况,我编写了一个开源库:EntityFramework.Exceptions它允许您捕获如下类型的异常:
using (var demoContext = new DemoContext())
{
demoContext.Products.Add(new Product
{
Name = "a",
Price = 1
});
demoContext.Products.Add(new Product
{
Name = "a",
Price = 1
});
try
{
demoContext.SaveChanges();
}
catch (UniqueConstraintException e)
{
//Handle exception here
}
}
Run Code Online (Sandbox Code Playgroud)
您所要做的就是从 Nuget 安装它并在您的OnConfiguring方法中调用它:
class DemoContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<ProductSale> ProductSale { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseExceptionProcessor();
}
}
Run Code Online (Sandbox Code Playgroud)
您无法检测重复键违规的原因是您使用 dbcontext 的单个实例来保存重复数据。让我用一个示例控制器来解释一下:
MyController.cs
public class MyController : Controller
{
private readonly MyDbContext _context;
public MyController(MyDbContext context)
{
_context = context;
}
public IActionResult AddFirst()
{
var user = new User
{
Name = "Alice"
};
_context.Users.Add(user);
var team = new Team
{
Id = "uniqueteamid",
Name = "A Team"
};
_context.Teams.Add(team);
var teamuser1 = new TeamUser()
{
User = user,
Team = team
};
_context.TeamUsers.Add(teamuser1);
_context.SaveChanges();
return View();
}
public IActionResult AddSecond()
{
var teamuser2 = new TeamUser()
{
UserName = "Alice",
TeamId = "uniqueteamid"
};
_context.TeamUsers.Add(teamuser2);
_context.SaveChanges();
return View();
}
public IActionResult AddFirstAndSecond()
{
var user = new User
{
Name = "Bob"
};
_context.Users.Add(user);
var team = new Team
{
Id = "anotherteamid",
Name = "B Team"
};
_context.Teams.Add(team);
var teamuser1 = new TeamUser()
{
User = user,
Team = team
};
_context.TeamUsers.Add(teamuser1);
var teamuser2 = new TeamUser()
{
User = user,
Team = team
};
_context.TeamUsers.Add(teamuser2);
_context.SaveChanges();
return View();
}
public IActionResult AddFirstAndSecondAgain()
{
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=aspnet-WebApplication1;Trusted_Connection=True;MultipleActiveResultSets=true");
using (var context = new MyDbContext(optionsBuilder.Options))
{
var user = new User
{
Name = "Cat"
};
context.Users.Add(user);
context.SaveChanges();
}
using (var context = new MyDbContext(optionsBuilder.Options))
{
var team = new Team
{
Id = "andanotherteamid",
Name = "C Team"
};
context.Teams.Add(team);
context.SaveChanges();
}
using (var context = new MyDbContext(optionsBuilder.Options))
{
var teamuser1 = new TeamUser()
{
UserName = "Cat",
TeamId = "andanotherteamid"
};
context.TeamUsers.Add(teamuser1);
context.SaveChanges();
}
using (var context = new MyDbContext(optionsBuilder.Options))
{
var teamuser2 = new TeamUser()
{
UserName = "Cat",
TeamId = "andanotherteamid"
};
context.TeamUsers.Add(teamuser2);
context.SaveChanges();
}
return View();
}
}
Run Code Online (Sandbox Code Playgroud)
在该控制器中,有 4 个操作方法:AddFirst、AddSecond、AddFirstAndSecond、 和AddFirstAndSecondAgain。
情况1(AddFirst和AddSecond):
假设AddFirst首先被调用。这将创建一个新用户、一个新团队和一个团队用户。现在,如果AddSecond之后调用,这将尝试添加重复的 TeamUser 并抛出重复的键违规异常。原因是第二次插入重复 TeamUser 的调用使用与第一次插入 TeamUser 的调用不同的 dbcontext 实例。
情况2 ( AddFirstAndSecond):
假设你打电话给AddFirstAndSecond. 这将抛出无效操作异常。为什么?因为您使用 dbcontext 的单个实例来添加第一个 TeamUser 和第二个重复的 TeamUser。Entity Framework Core 已在跟踪第一个 TeamUser,因此无法跟踪第二个重复的 TeamUser。
情况3(AddFirstAndSecondAgain):
如果您确实需要在单个操作方法中添加重复的 TeamUser,则在添加每个 TeamUser 时需要使用不同的 dbcontext 实例。看看AddFirstAndSecondAgain动作方法。这还会引发重复键冲突异常,因为您使用 dbcontext 的不同实例来添加第一个和第二个重复的 TeamUser。