Modifier l’arbre d’expression de IQueryable.Include () pour append une condition à la jointure

En gros, j’aimerais implémenter un référentiel qui filtre tous les enregistrements supprimés à l’aide de logiciels même via les propriétés de navigation. J’ai donc une entité de base, quelque chose comme ça:

public abstract class Entity { public int Id { get; set; } public bool IsDeleted { get; set; } ... } 

Et un référentiel:

 public class BaseStore : IStore where TEntity : Entity { protected readonly ApplicationDbContext db; public IQueryable GetAll() { return db.Set().Where(e => !e.IsDeleted) .InterceptWith(new InjectConditionVisitor(entity => !entity.IsDeleted)); } public IQueryable GetAll(Expression<Func> predicate) { return GetAll().Where(predicate); } public IQueryable GetAllWithDeleted() { return db.Set(); } ... } 

La fonction InterceptWith est issue de ces projets: https://github.com/davidfowl/QueryInterceptor et https://github.com/StefH/QueryInterceptor (idem avec les implémentations asynchrones)

Une utilisation d’un IStore se présente comme IStore :

 var project = await ProjectStore.GetAll() .Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId); 

J’ai implémenté un ExpressionVisitor:

 internal class InjectConditionVisitor : ExpressionVisitor { private Expression<Func> queryCondition; public InjectConditionVisitor(Expression<Func> condition) { queryCondition = condition; } public override Expression Visit(Expression node) { return base.Visit(node); } } 

Mais c’est le point où je me suis retrouvé coincé. Je mets un point d’arrêt dans la fonction de visite pour voir quelles expressions j’ai, et quand devrais-je faire quelque chose de malicieux, mais cela n’atteint jamais la partie Inclure (p => p.Versions) de mon arbre.

J’ai vu d’autres solutions qui peuvent fonctionner, mais elles sont “permanentes”, par exemple EntityFramework.Filters semblait être bon pour la plupart des cas d’utilisation, mais vous devez append un filtre lorsque vous configurez le DbContext – cependant, vous pouvez désactiver les filtres, mais je ne veux pas désactiver et réactiver un filtre pour chaque requête. Une autre solution comme celle-ci consiste à souscrire à l’événement ObjectMaterialized d’ObjectContext, mais je ne l’aimerais pas non plus.

Mon objective serait de “capturer” les inclus dans le visiteur et de modifier l’arborescence des expressions pour append une autre condition à la jointure qui vérifie le champ IsDeleted de l’enregistrement uniquement si vous utilisez l’une des fonctions GetAll du magasin. Toute aide serait appréciée!

Mettre à jour

Le but de mes référentiels est de masquer certains comportements de base de l’entité de base – il contient également “créé / lastmodifié par”, “created / lastmodified-date”, horodatage, etc. Mon BLL récupère toutes les données à travers ce référentiel. pas besoin de vous soucier de ceux-ci, le magasin gérera toutes les choses. Il existe également une possibilité d’hériter du BaseStore pour une classe spécifique (mon BaseStore configuré configurera alors l’injection dans la classe héritée dans IStore s’il existe), où vous pouvez append un comportement spécifique. Par exemple, si vous modifiez un projet, vous devez append ces modifications historiques, puis vous ajoutez simplement cela à la fonction de mise à jour du magasin hérité.

Le problème commence lorsque vous interrogez une classe qui possède des propriétés de navigation (donc n’importe quelle classe: D). Il y a deux entités concrètes:

  public class Project : Entity { public ssortingng Name { get; set; } public ssortingng Description { get; set; } public virtual ICollection Platforms { get; set; } //note: this version is not historical data, just the versions of the project, like: 1.0.0, 1.4.2, 2.1.0, etc. public virtual ICollection Versions { get; set; } } public class Platform : Entity { public ssortingng Name { get; set; } public virtual ICollection Projects { get; set; } public virtual ICollection TestFunctions { get; set; } } public class ProjectVersion : Entity { public ssortingng Code { get; set; } public virtual Project Project { get; set; } } 

Donc, si je veux lister les versions du projet, j’appelle le magasin: await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId) . Je n’obtiendrai pas le projet supprimé, mais si le projet existe, il renverra toutes les versions qui lui sont associées, même celles supprimées. Dans ce cas particulier, je pourrais commencer par l’autre côté et appeler le ProjectVersionStore, mais si je souhaite interroger deux propriétés de navigation ou plus, le jeu se termine à la fin 🙂

Le comportement attendu serait le suivant: si j’inclue les versions dans le projet, il ne devrait interroger que les versions non supprimées, de sorte que la jointure SQL générée contienne également une [Versions].[IsDeleted] = FALSE . C’est encore plus compliqué avec des inclusions complexes comme Include(project => project.Platforms.Select(platform => platform.TestFunctions)) .

La raison pour laquelle j’essaie de le faire de cette façon est que je ne veux pas refactoriser tous les Include dans la BLL en quelque chose d’autre. C’est la partie paresseuse 🙂 L’autre est que je voudrais une solution transparente, je ne veux pas que le BLL sache tout cela. L’interface doit restr inchangée si cela n’est pas absolument nécessaire. Je sais que c’est juste une méthode d’extension, mais ce comportement devrait être dans la couche magasin.

La méthode include que vous utilisez appelle la méthode QueryableExtensions.Include (source, path1) qui transforme l’expression en un chemin de chaîne. Voici ce que fait la méthode include:

 public static IQueryable Include(this IQueryable source, Expression> path) { Check.NotNull>(source, "source"); Check.NotNull>>(path, "path"); ssortingng path1; if (!DbHelpers.TryParsePath(path.Body, out path1) || path1 == null) throw new ArgumentException(Ssortingngs.DbExtensions_InvalidIncludePathExpression, "path"); return QueryableExtensions.Include(source, path1); } 

Donc, votre expression ressemble à ceci (cochez la méthode “Include” ou “IncludeSpan” dans votre expression):

  value(System.Data.Entity.Core.Objects.ObjectQuery`1[TEntity]).MergeAs(AppendOnly) .IncludeSpan(value(System.Data.Entity.Core.Objects.Span)) 

Vous devriez accrocher VisitMethodCall pour append votre expression à la place:

 internal class InjectConditionVisitor : ExpressionVisitor { private Expression> queryCondition; protected override Expression VisitMethodCall(MethodCallExpression node) { Expression expression = node; if (node.Method.Name == "Include" || node.Method.Name == "IncludeSpan") { // DO something here! Let just add an OrderBy for fun // LAMBDA: x => x.[PropertyName] var parameter = Expression.Parameter(typeof(T), "x"); Expression property = Expression.Property(parameter, "ColumnInt"); var lambda = Expression.Lambda(property, parameter); // EXPRESSION: expression.[OrderMethod](x => x.[PropertyName]) var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == "OrderBy" && x.GetParameters().Length == 2); var orderByMethodGeneric = orderByMethod.MakeGenericMethod(typeof(T), property.Type); expression = Expression.Call(null, orderByMethodGeneric, new[] { expression, Expression.Quote(lambda) }); } else { expression = base.VisitMethodCall(node); } return expression; } } 

Le projet QueryInterceptor de David Fowl ne prend pas en charge “Inclure”. Entity Framework essaie de trouver la méthode “Include” à l’aide de la reflection et renvoie la requête en cours si elle n’est pas trouvée (ce qui est le cas).

Disclaimer : Je suis le propriétaire du projet EF + .

J’ai ajouté une fonctionnalité QueryInterceptor qui prend en charge “Inclure” pour répondre à votre question. La fonctionnalité n’est pas encore disponible car le test unitaire n’a pas été ajouté, mais vous pouvez télécharger et essayer la source: Query Interceptor Source

Contactez-moi directement (e-mail au bas de ma page d’accueil GitHub) si vous rencontrez un problème, sinon le sujet commencera à être hors sujet.

Attention, la méthode “Include” modifie l’expression en masquant certaines expressions précédentes. Il est donc parfois difficile de comprendre ce qui se passe réellement sous le capot.

Mon projet contient également une fonctionnalité de filtre de requête qui, à mon avis, offre davantage de flexibilité.


EDIT: Ajouter un exemple de travail de mis à jour requirejs

Voici un code de départ que vous pouvez utiliser pour vos besoins:

 public IQueryable GetAll() { var conditionVisitor = new InjectConditionVisitor("Versions", db.Set.Provider, x => x.Where(y => !y.IsDeleted)); return db.Set().Where(e => !e.IsDeleted).InterceptWith(conditionVisitor); } var project = await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId); internal class InjectConditionVisitor : ExpressionVisitor { private readonly ssortingng NavigationSsortingng; private readonly IQueryProvider Provider; private readonly Func, IQueryable> QueryCondition; public InjectConditionVisitor(ssortingng navigationSsortingng, IQueryProvider provder , Func, IQueryable> queryCondition) { NavigationSsortingng = navigationSsortingng; Provider = provder; QueryCondition = queryCondition; } protected override Expression VisitMethodCall(MethodCallExpression node) { Expression expression = node; bool isIncludeSpanValid = false; if (node.Method.Name == "IncludeSpan") { var spanValue = (node.Arguments[0] as ConstantExpression).Value; // The System.Data.Entity.Core.Objects.Span class and SpanList is internal, let play with reflection! var spanListProperty = spanValue.GetType().GetProperty("SpanList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var spanList = (IEnumerable)spanListProperty.GetValue(spanValue); foreach (var span in spanList) { var spanNavigationsField = span.GetType().GetField("Navigations", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); var spanNavigation = (List)spanNavigationsField.GetValue(span); if (spanNavigation.Contains(NavigationSsortingng)) { isIncludeSpanValid = true; break; } } } if ((node.Method.Name == "Include" && (node.Arguments[0] as ConstantExpression).Value.ToSsortingng() == NavigationSsortingng) || isIncludeSpanValid) { // CREATE a query from current expression var query = Provider.CreateQuery(expression); // APPLY the query condition query = QueryCondition(query); // CHANGE the query expression expression = query.Expression; } else { expression = base.VisitMethodCall(node); } return expression; } } 

EDIT: Répondre aux sous-questions

Différence entre Include et IncludeSpan

D’après ce que j’ai compris

IncludeSpan: Apparaît lorsque la requête d’origine n’a pas encore été modifiée par une méthode LINQ.

Inclure: Apparaît lorsque la requête d’origine a été modifiée par une méthode LINQ (vous ne voyez plus l’expression précédente)

 -- Expression: {value(System.Data.Entity.Core.Objects.ObjectQuery`1[Z.Test.EntityFramework.Plus.Association_Multi_OneToMany_Left]).MergeAs(AppendOnly).IncludeSpan(value(System.Data.Entity.Core.Objects.Span))} var q = ctx.Association_Multi_OneToMany_Lefts.Include(x => x.Right1s).Include(x => x.Right2s); -- Expression: {value(System.Data.Entity.Core.Objects.ObjectQuery`1[Z.Test.EntityFramework.Plus.Association_Multi_OneToMany_Left]).Include("Right2s")} var q = ctx.Association_Multi_OneToMany_Lefts.Include(x => x.Right1s).Where(x => x.ColumnInt > 10).Include(x => x.Right2s); 

Comment inclure et filtrer des entités associées

Inclure ne vous permet pas de filtrer les entités associées. Vous pouvez trouver 2 solutions dans ce post: EF. Comment inclure uniquement certains résultats secondaires dans un modèle?

  • L’une implique l’utilisation d’une projection
  • L’une implique l’utilisation de EF + Query IncludeFilter à partir de ma bibliothèque.