更新EFCore连接的通用方法

时间:2018-02-14 08:16:06

标签: c# entity-framework entity-framework-core

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

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

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

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

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<PersonCar> PersonCars { get; set; } = new List<PersonCar>();
}

public class Car
{
    public int Id { get; set; }
    public string Manufacturer { get; set; }

    public virtual ICollection<PersonCar> PersonCars { get; set; } = new List<PersonCar>();
}

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<PersonCar>().HasKey(t => new { t.PersonId, t.CarId });

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

var person = new Person
{
    Name = "John",
    PersonCars = new List<PersonCar>
    {
        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<T, TJoin, Touter>(PersonCarDbContext db, int entityId, int[] nestedids)
{
    //.. do same logic but with reflection?
}

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

这可行吗?我无法看到如何做到但它看起来像是可能的事情。

1 个答案:

答案 0 :(得分:6)

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

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

public static void UpdateLinks<TLink>(this DbSet<TLink> dbSet, 
    Expression<Func<TLink, int>> fromIdProperty, int fromId, 
    Expression<Func<TLink, int>> toIdProperty, int[] toIds)
    where TLink : class, new()
{
    // link => link.FromId == fromId
    var filter = Expression.Lambda<Func<TLink, bool>>(
        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<Func<int, TLink>>(
        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属性的表达式,以及所需的值。属性选择器表达式用于动态构建查询过滤器以及编写和编译仿函数以创建和初始化新的链接实体。

代码并不难,但需要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();
    }
}