efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Allow forcing a constant in query filters

Open stevendarby opened this issue 2 years ago • 19 comments

I've been looking for a way to not parameterise values used in query filters. One use-case for this could be to add TenantId filters to entities in a multi-tenant DB. Tenant datasets may differ wildly and parameter sniffing can make a query plan generated for one tenant not suitable for another. A query plan for each tenant could be achieved via non-parameterised filters.

Using this example use-case, I tried passing the tenant ID value used in the filters through this custom function that uses [NotParameterized], but the expression that comes into the HasTranslation function is still SqlParameterExpression and so still produces a parameter.

public int DeParameterize([NotParameterized] int tenantId) =>
    throw new NotSupportedException("For SQL translation only");

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .HasDbFunction(GetType().GetMethod(nameof(DeParameterize))!)
        .HasTranslation(x => x.First());
}

I confirmed the above approach works if I use it in a manual filter at query time, just not when used in a query filter at model building time. It would be really handy to be able to do this. Is there another way?

Tried on EF Core 6.0 preview 7.

Full example
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.Logging;

var tenantId = 1;
using (var db = new BloggingContext())
{
    //db.Database.EnsureCreated();
    _ = db.Blogs.FirstOrDefault(x => x.TenantId == db.DeParameterize(tenantId));
}

using (var db = new BloggingContextWithFilter(tenantId))
{
    _ = db.Blogs.FirstOrDefault();
}


public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }

    public int DeParameterize([NotParameterized] int tenantId) =>
        throw new NotSupportedException("For SQL translation only");

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(@"Server=.;Database=DeParameterize;Integrated Security=True")
            .EnableDetailedErrors()
            .EnableSensitiveDataLogging()
            .LogTo(x => Debug.WriteLine(x), LogLevel.Information);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .HasDbFunction(GetType().GetMethod(nameof(DeParameterize))!)
            .HasTranslation(x => x.First());
    }
}

public class BloggingContextWithFilter : BloggingContext
{
    private readonly int _tenantId;

    public BloggingContextWithFilter(int tenantId)
    {
        _tenantId = tenantId;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().HasQueryFilter(x => x.TenantId == DeParameterize(_tenantId));

        base.OnModelCreating(modelBuilder);
    }
}

public class Blog
{
    public int Id { get; set; }
    public int TenantId { get; set; }
}

stevendarby avatar Aug 20 '21 19:08 stevendarby