How can I intercept elements in an IQueryable regardless of how many transforms are applied?

Viewed 157

I need to wrap an IQueryable<T> so that when the result is finally iterated I will have access to each element as it is returned.

To achieve this, I tried creating a QueryableWrapper<T> class which acts as a man-in-the-middle for the real queryable instance.

Then, when my repository is asked for its IQueryable<T> I returned this wrapper instead. This works fine until the consumer applies any further LINQ statements such as Where etc.

What I need to do is have my Repository intercept every item returned from the query as the consumer iterates it. This is so I can do the following

  1. Is the result a domain object?
  2. If so, then keep track of the object in case it is modified

I managed to get a bit further by also wrapping the result of Provider - but this crashes when the consumer does an OrderBy, because my result is not an IOrderedEnumerable.

I could implement this, but then I fear I am going down a rabbit hole, and there should be an easier way to achieve this.

    internal class QueryableWrapper<T> : IQueryable<T>
    {
        private readonly IQueryable<T> Source;
        private readonly IQueryProvider QueryProvider;

        public QueryableWrapper(IQueryable<T> source)
        {
            CollectionName = collectionName;
            DbContext = dbContext;
            Source = source;
            QueryProvider = new QueryProviderWrapper<T>(source.Provider);
        }

        public Type ElementType => Source.ElementType;

        public Expression Expression => Source.Expression;

        public IQueryProvider Provider => QueryProvider;

        public IEnumerator<T> GetEnumerator() =>
            new EnumeratorWrapper<T>(Source.GetEnumerator());

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }


    internal class EnumeratorWrapper<T> : IEnumerator<T>
    {
        private readonly IEnumerator<T> Source;

        public EnumeratorWrapper(IEnumerator<T> source)
        {
            Source = source;
        }

        public T Current => Source.Current;

        object? IEnumerator.Current => Current;

        public void Dispose()
        {
            Source.Dispose();
        }

        public bool MoveNext()
        {
            bool result = Source.MoveNext();
            if (result)
            {
                Console.WriteLine("Intercepted " + Source.Current.GetType().FullName);
            }
            return result;
        }

        public void Reset()
        {
            Source.Reset();
        }
    }

    internal class QueryProviderWrapper<T> : IQueryProvider
    {
        private readonly IQueryProvider Source;

        public QueryProviderWrapper(IQueryProvider source)
        {
            Source = source;
        }

        public IQueryable CreateQuery(Expression expression) => CreateQuery<T>(expression);

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            IQueryable<TElement> result = Source.CreateQuery<TElement>(expression);
            return new QueryableWrapper<TElement>(source: result);
        }

        public object? Execute(Expression expression) => Execute<T>(expression);

        public TResult Execute<TResult>(Expression expression) => Source.Execute<TResult>(expression);
    }
}
internal class QueryableWrapper<T> : IQueryable<T>
{
    private readonly IQueryable<T> Source;

    public QueryableWrapper(IQueryable<T> source)
    {
        Source = source;
    }

    public Type ElementType => Source.ElementType;

    public Expression Expression => Source.Expression;

    public IQueryProvider Provider => Source.Provider;

    public IEnumerator<T> GetEnumerator() =>
        new EnumeratorWrapper<T>(Source.GetEnumerator());

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}


internal class EnumeratorWrapper<T> : IEnumerator<T>
{
    private readonly IEnumerator<T> Source;

    public EnumeratorWrapper(IEnumerator<T> source)
    {
        Source = source;
    }

    public T Current => Source.Current;

    object? IEnumerator.Current => Source.Current;

    public void Dispose()
    {
        Source.Dispose();
    }

    public bool MoveNext()
    {
        bool result = Source.MoveNext();
        if (result)
            Console.WriteLine("Intercepted!");
        return result;
    }

    public void Reset()
    {
        Source.Reset();
    }
}
2 Answers

It's simply because orderBy in queryable.OrderBy will return a new IQueryable that is not wrapped. The new MoveNext is from the new instance provided by the new function.

In your foreach, the one used is the unwrapped.

Try this:

var data = new List<string> { "A", "B" };
var queryable = new QueryableWrapper<string>(data.AsQueryable().OrderBy(x => x));
foreach (string datum in queryable)
    Console.WriteLine("Ordered datum = " + datum);

Create the full queryable expression and then create the wrapper around the final result to have it intercept it.

The documentation explains it: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution

After a LINQ query is created by a user, it is converted to a command tree. A command tree is a representation of a query that is compatible with the Entity Framework. The command tree is then executed against the data source

At what point query expressions are executed can vary. LINQ queries are always executed when the query variable is iterated over, not when the query variable is created. This is called deferred execution. You can also force a query to execute immediately, which is useful for caching query results. This is described later in this topic.

IOrderedEnumerable<T> is just a tag class. I was able to tag my IQueryableWrapper<T> with that interface without implementing any code and everything worked!

The final code is as follows:

public class MyRepository<TWhatever>
{
    public IQueryable<TWhatever> Query() =>
        new QueryableWrapper<TWhatever>(TheRealOne);
}
namespace DomainDrivenDesign.MongoDB.Persistence
{
    internal class QueryableWrapper<T> : IQueryable<T>, IOrderedQueryable<T>
    {
        private readonly IQueryable<T> Source;
        private readonly Lazy<IQueryProvider> QueryProvider;

        public QueryableWrapper(IQueryable<T> source)
        {
            Source = source;
            QueryProvider = new Lazy<IQueryProvider>(() =>
                new QueryProviderWrapper<T>(collectionName, dbContext, source.Provider));
        }

        public Type ElementType => Source.ElementType;

        public Expression Expression => Source.Expression;

        public IQueryProvider Provider => QueryProvider.Value;

        public IEnumerator<T> GetEnumerator() => new EnumeratorWrapper<T>(Source.GetEnumerator());

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }


    internal class EnumeratorWrapper<T> : IEnumerator<T>
    {
        private readonly IEnumerator<T> Source;

        public EnumeratorWrapper(IEnumerator<T> source)
        {
            CollectionName = collectionName;
            DbContext = dbContext;
            Source = source;
        }

        public T Current => Source.Current;

        object? IEnumerator.Current => Current;

        public void Dispose()
        {
            Source.Dispose();
        }

        public bool MoveNext()
        {
            bool result = Source.MoveNext();
            if (result)
            {
                Console.WriteLine("Intercepted!");
            }
            return result;
        }

        public void Reset()
        {
            Source.Reset();
        }
    }

    internal class QueryProviderWrapper<T> : IQueryProvider
    {
        private readonly IQueryProvider Source;

        public QueryProviderWrapper(IQueryProvider source)
        {
            Source = source;
        }

        public IQueryable CreateQuery(Expression expression) => CreateQuery<T>(expression);

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            IQueryable<TElement> result = Source.CreateQuery<TElement>(expression);
            return new QueryableWrapper<TElement>(result);
        }

        public object? Execute(Expression expression) => Execute<T>(expression);

        // Note that this bypasses Enumerator if the query ends with a scalar
        // such as .First()
        public TResult Execute<TResult>(Expression expression) => Source.Execute<TResult>(expression);
    }
}
Related