Méthode générique de mise à jour des jointures EFCore

Une des choses que je trouve vraiment fastidieuse avec la façon dont EFCore gère les relations plusieurs à plusieurs est la mise à jour des collections jointes d’entités. Il est souvent nécessaire qu’un modèle de vue vienne du serveur avec une nouvelle liste d’entités nestedes et je dois écrire une méthode pour chaque entité nestede qui détermine ce qui doit être supprimé, ce qui doit être ajouté, puis effectue les suppressions et ajouts . Parfois, une entité a plusieurs relations plusieurs-à-plusieurs et je dois écrire à peu près le même code pour chaque collection.

Je pense qu’une méthode générique pourrait être utilisée ici pour m’empêcher de me répéter, mais j’ai du mal à comprendre comment.

Tout d’abord, laissez-moi vous montrer comment je le fais actuellement.

Disons que nous avons ces modèles:

public class Person { public int Id { get; set; } public ssortingng Name { get; set; } public virtual ICollection PersonCars { get; set; } = new List(); } public class Car { public int Id { get; set; } public ssortingng 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; } } 

Et une clé définie avec une API fluide

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

Et nous ajoutons une nouvelle personne et une liste des voitures associées:

 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(); 

John possède des voitures 1,2,3 . John met à jour ses voitures sur le front-end. Une nouvelle liste d’identifiants de voitures est transmise. Le code actuel utilise un modèle et appelle probablement une méthode comme celle-ci:

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

Je travaille sur ceux à supprimer, ceux à append, puis faire les actions. Toutes les choses très simples, mais dans le monde réel, une entité peut avoir plusieurs collections de plusieurs à plusieurs, c.-à-d. Des balises, des catégories, des options, etc. Chaque méthode de mise à jour est à peu près identique et je me retrouve avec le même code répété plusieurs fois. Par exemple, disons que la personne avait également une relation plusieurs-à-plusieurs dans la 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(); } } 

C’est exactement le même code qui fait référence à différents types et propriétés. Je me retrouve avec ce code répété de nombreuses fois. Est-ce que je fais mal ou est-ce un bon cas pour une méthode générique?

Je pense que c’est un bon endroit pour utiliser un générique, mais je ne vois pas trop comment le faire.

Il aura besoin du type d’entité, du type d’entité de jointure et du type d’entité externe.

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

Méthode déterminera ensuite la propriété appropriée et effectuera les suppressions et les ajouts requirejs.

Est-ce faisable? Je ne vois pas comment le faire, mais cela ressemble à quelque chose de possible.

“Tout ce qui est très simple” , mais pas si simple à factoriser, en particulier si l’on prend en compte différents types de clé, les propriétés FK explicites ou fantômes, etc., tout en conservant les arguments de méthode minimaux.

Voici la meilleure méthode factorisée à laquelle je puisse penser, qui fonctionne pour les entités link (join) ayant 2 FK int explicites:

 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.Comstack(); 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.Comstack()); dbSet.RemoveRange(deleteLinks); dbSet.AddRange(addLinks); } 

Tout ce dont il a besoin est l’entité de DbSet , deux expressions représentant les propriétés FK et les valeurs souhaitées. Les expressions de sélecteur de propriétés sont utilisées pour créer de manière dynamic des filtres de requête, ainsi que pour composer et comstackr un foncteur afin de créer et d’initialiser une nouvelle entité de lien.

Le code n’est pas si difficile, mais nécessite la connaissance des méthodes System.Linq.Expressions.Expression .

La seule différence avec le code manuscrit est que

 Expression.Constant(fromId) 

Dans l’expression de filter , EF génère une requête SQL avec une valeur constante plutôt qu’un paramètre, ce qui empêche la mise en cache du plan de requête. Il peut être corrigé en remplaçant ce qui précède par

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

Cela étant dit, l’utilisation de votre échantillon ressemblerait à ceci:

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

et aussi autrement:

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