c# - 更新 EFCore 连接的通用方法

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

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

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

首先让我向您展示我目前的做法。

假设我们有这些模型:

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

还有一个用 fluent API 定义的键

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

然后我们添加一个新的 Person 和相关汽车列表:

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

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

这可行吗?我看不出该怎么做,但看起来是可行的。

最佳答案

“都是非常简单的东西”,但分解起来并不那么简单,尤其是考虑到不同的键类型、显式或影子 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();
    }
}

关于c# - 更新 EFCore 连接的通用方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48782491/

相关文章:

c# - 从 XML 模式生成 C# 类文件

asp.net - Entity Framework 7 - 访问相关实体

entity-framework - Entity Framework 迁移如何知道数据库处于哪个版本?

c# - Entity Framework Core jsonb 列类型

c# - Linq to Xml,是否可以改进此查询?

c# - .NET C# 在父接口(interface)中显式实现祖 parent 的接口(interface)方法

c# - 使用正则表达式删除整个 Html 中的空格,但在 pre 内部

entity-framework - Azure 源代码控制部署未运行我最新的 Code First 迁移

c# - 使用 Entity Framework Core 时是否应该处理 DbContext

c# - Entity Framework Core – “Insert if not exists” 可能吗?