Search code examples
c#.net-coreentity-framework-core.net-core-3.1ef-core-3.1

.NET Core Self Accessing QueryFilters


I'm looking for a work around for an EntityFramework Core Bug. I'm trying to write a query which filters on itself.

Disclaimer: I'm doing something a bit more complex than filtering by an explicit userId, I am just using this with a hard-coded value for simplicity, as the exact implementation isn't relevant to my question.

protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.Entity<ConversationSubscription>()
        .HasQueryFilter(x => x.Conversation.ConversationSubscriptions
            .Select(c => c.UserId).Contains(315)
        );

Since the Query Filter is attempting to access the entity in which it's filtering, it ends up in and endless loop. Since we're working with the ModelBuilder and not the DbSet, there is no way to mark it as IgnoreQueryFilters.

Given this, I tried to use the current context to filter itself:

modelBuilder.Entity<ConversationSubscription>().HasQueryFilter(x => 
    this.Set<ConversationSubscription().AsNoTracking().IgnoreQueryFilters()
        .Where(cs => cs.ConversationId == x.ConversationId)
            .Select(c => c.UserId)
            .Contains(315)
);

However, this throws an InvalidOperationException, most likely because we're attempting to use the context before OnModelCreating has finished.

I feel like there is a way I can hack around this if I can somehow select the ConversationSubsriptions into an Anonymous Type, such that they're unfiltered.

Edit: I tried to hack around this using an anonymous type, but no luck.

modelBuilder.Entity<ConversationSubscription>().HasQueryFilter(c =>
    x => x.Conversation.Messages.Select(m => new {
        Value = m.Conversation.ConversationSubscriptions.Distinct()
            .Select(cs => cs.UserId).Contains(c.Variable(this._userId)) 
    }).FirstOrDefault().Value
);

Solution

  • Query filters initially didn't support accessing navigation properties or db sets. Looks like EF Core 3.0 removed these limitations (probably because of the new Single SQL statement per LINQ query mode), with the following restrictions/bugs:

    1. AsNoTracking() and AsTracking() - not supported, which makes sense, since the query filter is always translated to SQL.

    2. Include / ThenInclude - allowed, but ignored by the same reason.

    3. IgnoreQueryFilters - not supported. This could be considered as bug since it could have been be used to resolve the next cases.

    4. Cross referencing filters (e.g. entity A filter uses entity B and entity B filter uses entity A) via either navigation properties or db sets - cause StackOverflowException because filters are trying to use each other. This is a bug.

    5. Self referencing filter via navigation properties - same bug as #4, should be like #6.

    6. Self referencing filter via db sets - supported(!), always ignored in filter subquery.

    With all that being said, luckily your case is supported by #6, i.e. your second attempt with just unsupported AsNoTracking() and IgnoreQueryFilters() removed:

    modelBuilder.Entity<ConversationSubscription>().HasQueryFilter(x => 
        this.Set<ConversationSubscription()
            .Where(cs => cs.ConversationId == x.ConversationId)
                  .Select(c => c.UserId)
                  .Contains(315));