0

In short, here's what I am trying to do: register a HasQueryFilter lambda expression for entities in a DbContext.OnModelCreating where I am enumerating an unknown set of entities that I DO know implement an interface.

The code (simplified for this example):

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes()) {
            Expression<Func<ITenantScoped, bool>> filter = e => contextAccessor.Get<ITenantContext>().CanAccessTenant(e.TenantId);
            modelBuilder.Entity(entityType.ClrType).HasQueryFilter(filter);
        }
    }

Some key notes:

  • ITenantScoped is an interface with one property TenantId.
  • contextAccessor.Get invokes an AsyncLocal and so it needs to be in the lambda to be called whenever it is invoked.
  • The intent of all this is to raise an exception if an entity with a TenantId I'm not allowed to access is found.

The problem: HasQueryFilter vomits an exception because it expects the filter to be of type Expression<Func<MyEntityType, bool>> (where MyEntityType is the actual type of the entity).

I know the type of the entity from the entityType.ClrType, BUT I'm at a loss as to how to convert this Lambda function to a type where the type is known at runtime and not compile time.

I'm fairly new to c#, so I might be missing something obvious (hoping so, actually).

Is there a way to do what I want here? I'm not married to the approach, but I do need to make sure that EF access to the entities is protected.

Scott
  • 888
  • 7
  • 21
  • DbContext has an edmx mapping file that maps the database tables/fields with the c# classes. It cannot be determined at runtime. The DbContext has to be defined for code to run. Since a DbContext there may be many different types of queries that use different tables in the database. But the mapping must be correct between the classes and the database for the query to run. The DbContext contains a list of objects which are the tables in the database. Each of the object is a class (the table in the database) and each class if a set of properties which are the columns in the database. – jdweng Jul 30 '22 at 23:35
  • In your code MyEntityType would be one table in the database. If you are implementing an interface is may contain multiple tables in the database. To make a generic interface you would need to pass the entire DbContext. – jdweng Jul 30 '22 at 23:38
  • Please see [this answer](https://stackoverflow.com/a/65702479/2501279) but I'm afraid while it can help resolve your current issue, it will not help you to achieve your goal cause I doubt that `contextAccessor.Get().CanAccessTenant(e.TenantId)` can be translated to SQL. – Guru Stron Jul 31 '22 at 00:17

1 Answers1

0

Turns out this answer (thanks to @Guru Stron in the comments under the question) does the trick to help translate/convert the LambdaExpression into the right types.

For convenience, here's the code snippet that worked for my case:

Expression<Func<ITenantScoped, bool>> filter = e => CheckTenantAccess(e, contextAccessor);
var param = Expression.Parameter(builder.Metadata.ClrType, "e");
var body = filter.Body;
var newFilter = Expression.Lambda(body, param);
builder.HasQueryFilter(newFilter);

where CheckTenantAccess encapsulated the complexity of the expression and allowed me to set more precise breakpoints for debugging.

Scott
  • 888
  • 7
  • 21