Modify the expression tree of IQueryable.Include() to add condition to the join

.net c# entity-framework-6 expression-trees linq

Question

Basically, I want to create a repository that filters all entries that have been gently removed, even with navigational settings. I thus have a basic object that looks like this:

public abstract class Entity
{
    public int Id { get; set; }

    public bool IsDeleted { get; set; }

    ...
}

Moreover, a repository

public class BaseStore<TEntity> : IStore<TEntity> where TEntity : Entity
{
    protected readonly ApplicationDbContext db;

    public IQueryable<TEntity> GetAll()
    {
        return db.Set<TEntity>().Where(e => !e.IsDeleted)
            .InterceptWith(new InjectConditionVisitor<Entity>(entity => !entity.IsDeleted));
    }

    public IQueryable<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate)
    {
        return GetAll().Where(predicate);
    }

    public IQueryable<TEntity> GetAllWithDeleted()
    {
        return db.Set<TEntity>();
    }

    ...
}

The projects https://github.com/davidfowl/QueryInterceptor and https://github.com/StefH/QueryInterceptor are where the InterceptWith function is found (same with async implementations)

an example ofIStore<Project> seems to be:

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

I put an ExpressionVisitor into action.

internal class InjectConditionVisitor<T> : ExpressionVisitor
{
    private Expression<Func<T, bool>> queryCondition;

    public InjectConditionVisitor(Expression<Func<T, bool>> condition)
    {
        queryCondition = condition;
    }

    public override Expression Visit(Expression node)
    {
        return base.Visit(node);
    }
}

But here is when I ran into trouble. I set a breakpoint in the Visit method so that I could see what expressions I had and when I should be sneaky, but it never reaches the Include(p => p.Versions) section of my tree.

While EntityFramework.Filters looked to be okay for the majority of use cases, you had to add a filter when creating the DbContext. You can deactivate filters, but I do not want to disable and reenable a filter for every query. I observed some other methods that could work, but they are "permanent". A similar alternative that I would not like is to listen to the ObjectContext's ObjectMaterialized event.

My objective is to "catch" the inclusions in the visitor and alter the expression tree to add another condition to the join that only checks the IsDeleted field of the record if one of the GetAll functions of the store is used. We would appreciate any assistance.

Update

My repositories include the "created/lastmodified by", "created/lastmodified-date," timestamp, etc. to disguise some of the underlying Entity's fundamental behavior. My BLL does not have to worry about them as it receives all the data from these repositories; the store will take care of everything. Additionally, there is a chance to inherit from theBaseStore for a certain class (then my set up DI will inject the inherited class intoIStore<Project> If it does, you may implement custom behavior there. If you want to include the modification history for a project, for instance, you just add it to the update method of the inherited store.

When you query a class that contains navigation properties, the issue arises (so any class :D ). There are two specific entities:

  public class Project : Entity 
  {
      public string Name { get; set; }

      public string Description { get; set; }

      public virtual ICollection<Platform> 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<ProjectVersion> Versions { get; set; }
  }

  public class Platform : Entity 
  {
      public string Name { get; set; }

      public virtual ICollection<Project> Projects { get; set; }

      public virtual ICollection<TestFunction> TestFunctions { get; set; }
  }

  public class ProjectVersion : Entity 
  {
      public string Code { get; set; }

      public virtual Project Project { get; set; }
  }

So I contact the shop to ask for a list of the project versions:await ProjectStore.GetAll().Include(p => p.Versions).SingleOrDefaultAsync(p => p.Id == projectId) . I won't get the deleted project, but if the project still exists, it will restore all of the Versions connected to it, even the ones that were destroyed. In this particular instance, I could begin on the other side and use the ProjectVersionStore, but if I wanted to query through more than two navigation properties, the game was over:)

If I add the Versions to the Project, it should only query the Versions that haven't been removed, hence the resultant sql join should have a[Versions].[IsDeleted] = FALSE also in condition. When intricate elements are added, such asInclude(project => project.Platforms.Select(platform => platform.TestFunctions)) .

I'm attempting to accomplish it in this manner because I don't want to change all of the Include's in the BLL. The lazy part is that: Another is that I want a transparent resolution and don't want the BLL to be aware of everything. If it is not absolutely required to update the interface, it should remain the same. This functionality belongs in the store layer, even though I realize it's merely an extension method.

1
2
1/25/2016 7:50:24 PM

Accepted Answer

You invoke the method QueryableExtensions with the include method. The expression is converted into a string path using the function Include(source, path1). The include technique does the following:

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

So, if you checked the "Include" or "IncludeSpan" method in your expression, it appears like this:

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

Instead, you need to hook on VisitMethodCall and add your expression:

internal class InjectConditionVisitor<T> : ExpressionVisitor
{
    private Expression<Func<T, bool>> 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;
    }
}

"Include" is not supported by David Fowl's QueryInterceptor project. In an effort to locate the "Include" function via reflection, Entity Framework returns the current query if it is unsuccessful (which is the case).

I am the project's owner, according to Disclaimer.

To address your query, I've implemented a QueryInterceptor functionality that supports "Include". Since the unit test has not yet been implemented, the functionality is not yet available, but you may download and try the code at Interceptor Source for Query.

If you have a problem, get in touch with me personally (my email is at the bottom of my GitHub webpage); otherwise, the conversation will go off subject.

Be cautious since the "Include" technique hides certain earlier expressions, changing the expression. Therefore, it might be challenging to grasp what is truly going on.

A Query Filter functionality is also part of my project, and I think it provides greater versatility.


EDIT: Add a functioning illustration from an upgraded version

Here is a sample program that you may use to fulfill your requirement:

public IQueryable<TEntity> GetAll()
{
    var conditionVisitor = new InjectConditionVisitor<TEntity>("Versions", db.Set<TEntity>.Provider, x => x.Where(y => !y.IsDeleted));
    return db.Set<TEntity>().Where(e => !e.IsDeleted).InterceptWith(conditionVisitor);
}

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

internal class InjectConditionVisitor<T> : ExpressionVisitor
{
    private readonly string NavigationString;
    private readonly IQueryProvider Provider;
    private readonly Func<IQueryable<T>, IQueryable<T>> QueryCondition;

    public InjectConditionVisitor(string navigationString, IQueryProvider provder , Func<IQueryable<T>, IQueryable<T>> queryCondition)
    {
        NavigationString = navigationString;
        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<string>)spanNavigationsField.GetValue(span);

                if (spanNavigation.Contains(NavigationString))
                {
                    isIncludeSpanValid = true;
                    break;
                }
            }
        }

        if ((node.Method.Name == "Include" && (node.Arguments[0] as ConstantExpression).Value.ToString() == NavigationString)
            || isIncludeSpanValid)
        {

            // CREATE a query from current expression
            var query = Provider.CreateQuery<T>(expression);

            // APPLY the query condition
            query = QueryCondition(query);

            // CHANGE the query expression
            expression = query.Expression;
        }
        else
        {
            expression = base.VisitMethodCall(node);
        }

        return expression;
    }
}

EDIT: Respond to the subquestions

Include and IncludeSpan are different.

From what I gather

IncludeSpan: Shows up when a LINQ method hasn't yet changed the original query.

Include: Shows up when a LINQ method modifies the original query (You do not longer see previous expression)

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

How to add and remove connected things

You cannot filter related things using Include. There are two answers in this post: How can I just incorporate a subset of the results in a model?

  • One involves projecting something.
  • One involves using my library's EF+ Query IncludeFilter.
3
8/22/2018 10:30:09 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow