更新EFCore连接的通用方法

我发现EFCore处理多对多关系的方式非常乏味的一件事是更新实体连接集合。 经常要求视图模型来自前端,新的嵌套实体列表,我必须为每个嵌套实体编写一个方法,找出需要删除的内容,需要添加的内容然后执行删除和添加。 有时一个实体有多个多对多关系,我必须为每个集合写出几乎相同的代码。

我认为这里可以使用通用方法来阻止我重复自己,但我正在努力弄清楚如何。

首先让我告诉你我目前的做法。

让我们说我们有这些模型:

public class Person { public int Id { get; set; } public string Name { get; set; } public virtual ICollection PersonCars { get; set; } = new List(); } public class Car { public int Id { get; set; } public string Manufacturer { get; set; } public virtual ICollection PersonCars { get; set; } = new List(); } public class PersonCar { public virtual Person Person { get; set; } public int PersonId { get; set; } public virtual Car Car { get; set; } public int CarId { get; set; } } 

并且使用流畅的API定义了一个键

 modelBuilder.Entity().HasKey(t => new { t.PersonId, t.CarId }); 

我们添加一个新的人员和相关汽车列表:

 var person = new Person { Name = "John", PersonCars = new List { new PersonCar { CarId = 1 }, new PersonCar { CarId = 2 }, new PersonCar { CarId = 3 } } }; db.Persons.Add(person); db.SaveChanges(); 

约翰拥有1,2,3辆汽车。 John在前端更新他的汽车,所以现在我通过了一个新的汽车ID列表,所以我更新了这样的(实际代码将使用模型,并可能调用这样的方法):

 public static void UpdateCars(int personId, int[] newCars) { using (var db = new PersonCarDbContext()) { var person = db.Persons.Include(x => x.PersonCars).ThenInclude(x => x.Car).Single(x => x.Id == personId); var toRemove = person.PersonCars.Where(x => !newCars.Contains(x.CarId)).ToList(); var toAdd = newCars.Where(c => !person.PersonCars.Any(x => x.CarId == c)).ToList(); foreach (var pc in toRemove) { person.PersonCars.Remove(pc); } foreach (var carId in toAdd) { var pc = db.PersonCars.Add(new PersonCar { CarId = carId, PersonId = person.Id }); } db.SaveChanges(); } } 

我找出要删除的,要添加的然后执行操作。 所有非常简单的东西,但在现实世界中,实体可能具有多个多对多集合,即标签,类别,选项等。并且应用程序具有多个实体。 每个更新方法几乎完全相同,我最终重复相同的代码重复几次。 例如,假设Person也有一个Category实体多对多关系,它看起来像这样:

 public static void UpdateCategory(int personId, int[] newCats) { using (var db = new PersonCarDbContext()) { var person = db.Persons.Include(x => x.PersonCategories).ThenInclude(x => x.Category).Single(x => x.Id == personId); var toRemove = person.PersonCategories.Where(x => !newCats.Contains(x.CategoryId)).ToList(); var toAdd = newCats.Where(c => !person.PersonCategories.Any(x => x.CategoryId == c)).ToList(); foreach (var pc in toRemove) { person.PersonCategories.Remove(pc); } foreach (var catId in toAdd) { var pc = db.PersonCategories.Add(new PersonCategory { CategoryId = catId, PersonId = person.Id }); } db.SaveChanges(); } } 

它只是引用不同类型和属性的完全相同的代码。 我结束了这段代码重复了很多次。 我这样做是错的还是通用方法的好例子?

我觉得这是一个使用通用的好地方,但我不知道该怎么做。

它需要实体的类型,连接实体的类型和外部实体的类型,所以可能是这样的:

 public T UpdateJoinedEntity(PersonCarDbContext db, int entityId, int[] nestedids) { //.. do same logic but with reflection? } 

然后,方法将计算出正确的属性并执行所需的删除和添加。

这可行吗? 我看不出怎么做但它看起来像是可能的东西。

“所有非常简单的东西” ,但不是那么简单的分解,特别是考虑到不同的键类型,显式或阴影FK属性等,同时保持最小的方法参数。

这是我能想到的最好的分解方法,适用于具有2个显式int FK的链接(连接)实体:

 public static void UpdateLinks(this DbSet dbSet, Expression> fromIdProperty, int fromId, Expression> toIdProperty, int[] toIds) where TLink : class, new() { // link => link.FromId == fromId var filter = Expression.Lambda>( Expression.Equal(fromIdProperty.Body, Expression.Constant(fromId)), fromIdProperty.Parameters); var existingLinks = dbSet.Where(filter).ToList(); var toIdFunc = toIdProperty.Compile(); var deleteLinks = existingLinks .Where(link => !toIds.Contains(toIdFunc(link))); // toId => new TLink { FromId = fromId, ToId = toId } var toIdParam = Expression.Parameter(typeof(int), "toId"); var createLink = Expression.Lambda>( Expression.MemberInit( Expression.New(typeof(TLink)), Expression.Bind(((MemberExpression)fromIdProperty.Body).Member, Expression.Constant(fromId)), Expression.Bind(((MemberExpression)toIdProperty.Body).Member, toIdParam)), toIdParam); var addLinks = toIds .Where(toId => !existingLinks.Any(link => toIdFunc(link) == toId)) .Select(createLink.Compile()); dbSet.RemoveRange(deleteLinks); dbSet.AddRange(addLinks); } 

它所需要的只是连接实体DbSet ,两个表示FK属性的表达式以及所需的值。 属性选择器表达式用于动态构建查询filter以及编写和编译仿函数以创建和初始化新的链接实体。

代码并不难,但需要System.Linq.Expressions.Expression方法知识。

与手写代码的唯一区别是

 Expression.Constant(fromId) 

内部filter表达式将导致EF生成具有常量值而不是参数的SQL查询,这将阻止查询计划缓存。 它可以通过替换上面的方法来修复

 Expression.Property(Expression.Constant(new { fromId }), "fromId") 

话虽如此,您的样本使用情况如下:

 public static void UpdateCars(int personId, int[] carIds) { using (var db = new PersonCarDbContext()) { db.PersonCars.UpdateLinks(pc => pc.PersonId, personId, pc => pc.CarId, carIds); db.SaveChanges(); } } 

以及其他方式:

 public static void UpdatePersons(int carId, int[] personIds) { using (var db = new PersonCarDbContext()) { db.PersonCars.UpdateLinks(pc => pc.CarId, carId, pc => pc.PersonId, personIds); db.SaveChanges(); } }