Entity Framework is parameterizing my queries to leverage caching - c#

I'm working in .Net Core 2.1, creating an application which uses multitenancy. I'm applying default filters to my context. However, Entity Framework is not properly leveraging parametrized queries.
I have a configuration options being passed to my context to apply constraints which look like so:
public class ContextAuthorizationOptions : DbAuthorizationOptions<AstootContext>
{
protected IUserAuthenticationManager _userManager;
protected int _userId => this._userManager.GetUserId();
public ContextAuthorizationOptions(IUserAuthenticationManager authenticationManager, IValidatorProvider validatorProvider)
: base(validatorProvider)
{
this._userManager = authenticationManager;
ConstraintOptions.SetConstraint<Message>(x => x.Conversation.ConversationSubscriptions
.Select(cs => cs.UserId)
.Any(userId => userId == this._userId));
}
}
As you can see my query uses a property to store the userId value. My context takes in the constraint options ad applies them OnModels creating like so:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var constraintOptions = this._authorizationOptions.ConstraintOptions;
constraintOptions.ApplyStaticConstraint(modelBuilder);
base.OnModelCreating(modelBuilder);
}
My Model options look like so:
protected List<Action<ModelBuilder>> _constraints = new List<Action<ModelBuilder>>();
public void SetConstraint<T>(Expression<Func<T, bool>> constraint)
where T: class
{
this._constraints.Add(m => m.Entity<T>().HasQueryFilter(constraint));
}
public void ApplyStaticConstraint(ModelBuilder modelBuilder)
{
foreach(var applyConstraint in this._constraints)
{
applyConstraint(modelBuilder);
}
}
Since my filters are using properties I would expect this to generate a parameterized query yet when dumping to messages table to list it generates this SQL
SELECT [x].[Id], [x].[ConversationId], [x].[Created], [x].[MessageText], [x].[SenderUserId]
FROM [Messages] AS [x]
INNER JOIN [Conversations] AS [x.Conversation] ON [x].[ConversationId] = [x.Conversation].[Id]
WHERE EXISTS (
SELECT 1
FROM [ConversationSubscriptions] AS [cs]
WHERE ([cs].[UserId] = 2005) AND ([x.Conversation].[Id] = [cs].[ConversationId]))
How can I modify my implementation so Entity Framework Core can leverage query caching?

By some reason that only EF Core designers can explain, the query filter expressions are treated differently than the other query expressions. In particular, all variables which are not rooted to the target db context are evaluated and converted to constants. Rooted term has evolved from simply direct field/property of the context to more relaxed rules explained in #10301: Query: QueryFilter with EntityTypeConfiguration are failing to inject current context values Design meeting notes:
Patterns of configuration which would capture context correctly and inject current instance values
Defining filter in OnModelCreating
Defining filter in EntityTypeConfiguration by passing context through constructor
Defining filter using method (inside/outside DbContext or extension method) where context is passed as parameter.
Any of above where context is wrapped inside another object type and that type is being passed around.
Apart from above we will parametrize any kind of call on DbContext i.e. property/field access, method call, going through multiple levels.
The bullet #3 ("Defining filter using method (inside/outside DbContext or extension method) where context is passed as parameter.") leads me to a relatively simple generic solution.
Add the following simple class:
public static class Filter
{
public static T Variable<T>(this DbContext context, T value) => value;
}
Modify your options class like so:
protected List<Action<ModelBuilder, DbContext>> _constraints = new List<Action<ModelBuilder, DbContext>>();
public void SetConstraint<T>(Func<DbContext, Expression<Func<T, bool>>> constraint)
where T : class
{
this._constraints.Add((mb, c) => mb.Entity<T>().HasQueryFilter(constraint(c)));
}
public void ApplyStaticConstraint(ModelBuilder modelBuilder, DbContext context)
{
foreach (var applyConstraint in this._constraints)
{
applyConstraint(modelBuilder, context);
}
}
the SetConstraint call like so (note wrapping the this._userId into Variable method call):
ConstraintOptions.SetConstraint<Message>(c => x => x.Conversation.ConversationSubscriptions
.Select(cs => cs.UserId)
.Any(userId => userId == c.Variable(this._userId)));
and finally the ApplyStaticConstraint call:
constraintOptions.ApplyStaticConstraint(modelBuilder, this);
Now the query will use parameter instead of a constant value.

Related

Query DbContext during OnModelCreating for tenantId filtering [duplicate]

This question already has an answer here:
Queryfilter on ApplicationUser in OnModelCreating in ApplicationDbContext creates StackOverflowException
(1 answer)
Closed 2 months ago.
I am using .NET Core 5 with Entity Framework Core. I already have soft deletes by using SetQueryFilters with ActiveId to act as a delete.
I also want to apply a filter for certain tables that have a TenantId column to only show records that match the current user's organizations they are a part of (where TenantId falls within a list of OrganizationIds). I have the following code
public partial class AppContext : DbContext
{
public AppContext (DbContextOptions<AppContext> options)
: base(options)
{
}
#region Generated Properties
...
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var isActiveProperty = entityType.FindProperty("ActiveId");
var isTenantProperty = entityType.FindProperty("TenantId");
if (isTenantProperty != null && isTenantProperty.ClrType == typeof(Int32)
&& isActiveProperty != null && isActiveProperty.ClrType == typeof(Int32))
{
List<int> Ids = ///--Ids.NEEDTOGRABTHESE
var parameter = Expression.Parameter(entityType.ClrType, "p");
var filter = Expression.Lambda(
Expression.And(
Expression.OrElse(
Expression.Equal(Expression.Property(parameter, "ActiveId"), Expression.Constant((int)ActiveCodes.ACTIVE)),
Expression.Equal(Expression.Property(parameter, "ActiveId"), Expression.Constant((int)ActiveCodes.BLOCKED))
),
Expression.Equal(Expression.Property(parameter, "TenantId"), ///--Ids.NEEDTOGRABTHESE)
),
parameter);
entityType.SetQueryFilter(filter);//Update for ef 3.0
}
}
#region Generated Configuration
...
#endregion
}
}
I'm struggling to find out how to query the database inside the OnModelCreating. I need to query the database in order to get the current users organizations. If I try to reference the context while inside OnModelCreating, I get the following error:
An attempt was made to use the model while it was being created. A DbContext instance cannot be used inside 'OnModelCreating' in any way that makes use of the model that is being created.
Since the only way I can access the current users organization is by accessing the database, and the dbContext is inaccessible at this point, what can I do to access the dbContext during the OnModelCreating?
Also if you suggest something is it efficient enough to be called every time OnModelCreating is built (would it cache the response)?
If I am understanding your requirement correctly, you need to use HasQueryFilter API.
ex:
modelBuilder.Entity<Organization>().HasQueryFilter(org => org.UserId == _userId);
here is the link, for a better explanation. Hope this will help
You need to create another context for Tenants. The solution you are looking for is here:
Solution

Is it possible to implement an authorised DbContext?

Given a DbContext and a ClientContext (custom session data about the user) is it possible create a DbContext that is "authorised": where only a subset of the rows on each "table" is available?
With an authorised DbContext I'm trying to have a central row-level authorisation implementation.
I've researched it a bit and the only way to filter out a DbSet would be to use something like Queryable.Where but that returns an IQueryable<T> and there doesn't seem to be a way to return a filtered DbSet<T> (except maybe for global queries that you can setup in Startup but they don't have access to injected dependencies like ClientContext).
Is it possible to define DbSet<T> authorisation filters via an injected scoped dependency like ClientContext?
There are model-level query filters: https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-2.0#model-level-query-filters
From the link:
This feature allows LINQ query predicates (a boolean expression
typically passed to the LINQ Where query operator) to be defined
directly on Entity Types in the metadata model (usually in
OnModelCreating). Such filters are automatically applied to any LINQ
queries involving those Entity Types, including Entity Types
referenced indirectly, such as through the use of Include or direct
navigation property references.
Example from the link:
public class BloggingContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
public int TenantId { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>().HasQueryFilter(
p => !p.IsDeleted
&& p.TenantId == this.TenantId);
}
}
You can use this for simple scenarios. You define an instance property in your DbContext and in OnModelCreating you specify HasQueryFilter on any entity you want to filter. The property is an instance property, so if you have a scoped DbContext, the correct property value from that request would be used, which is handy if you want to filter by something from your UserContext. I have personally never tried this so I don't know how complex it allows your implementation to be, but you can play with it.
I'm not sure about EF and EF core, but we abstract the DbContext away into functional specific 'logic' blocks.
e.g:
class DbContext()
{
public DbSet<PeopleEntity> peoples;
}
class PeopleLogic()
{
DbContext _context;
PeopleLogic(DbContext context)
{
_context = context;
}
IEnumerable GetAllPeoples()
{
// create context,
// apply filters
// return result
}
}
We ofcourse have a base for simple CRUD operations;
public void AddOrUpdate(){
lock (SyncDatabaseWriteObject)
{
try
{
using (var context = CreateContext())
{
//insert the entity and add it to the db context
context.Set<TEntity>().AddOrUpdate((TEntity)entity);
context.SaveChanges();
}
return entity;
}
catch (Exception ex)
{
throw new DatabaseAccessException("Error occured while getting saving.", ex);
}
}
}
And instead of passing the dbcontext around, we pass around logics.
e.g. we seperate the logic for the database and the access to the database into 2 seperate projects, the business layer then only uses the dbAccess layer.

Access foreign key properties from IQueryable<T> in Entity Framework Core 3.1

I'm writing a IQueryable<T> extension method and I would like to get the foreign keys of the navigation properties of T. I have figured out that I can get access to them through IModel.FindEntityType(T).GetNavigations(), the question is how to best get access to the IModel while keeping the method as simple as possible.
At the moment the method looks something like this:
public static IQueryable<TQuery> DoMagic<TQuery>(this IQueryable<TQuery> query, IModel model)
{
var entity = model.FindEntityType(entityType);
var navigationProperties = entity.GetNavigations();
...
}
I would love if I wouldn't need to pass IModel as an argument like this:
public static IQueryable<TQuery> DoMagic<TQuery>(this IQueryable<TQuery> query)
{
var entity = Model.FindEntityType(entityType);
var navigationProperties = entity.GetNavigations();
...
}
I have considered adding a IServiceCollection extension method and passing the DbContext and setting IModel as a static property to the IQueryable<T> extension method. The problem with that is that it is limiting me to one DbContext.
I have also looked into the option to add a extension method to DbContextOptionsBuilder but haven't really figured out best way to achieve what I want to do this way.
Maybe there is other ways to get access to the foreign keys? Any help is greatly appreciated!
Edit
From the navigation properties I want to access the ForeignKey-property to know what property is used to resolve the navigation property. Something like:
navigationProperties.ToList().Select(x => x.ForeignKey);
One way of getting the navigation properties:
public static IQueryable<TQuery> DoMagic<TQuery>(this IQueryable<TQuery> query)
{
var navigationProperties = typeof(TQuery).GetProperties()
.Where(p => (typeof(IEnumerable).IsAssignableFrom(p.PropertyType) && p.PropertyType != typeof(string)))
.ToArray();
...
}
UPDATE:
Trying to get DbContext which has the Model property from IQueryable would result in some not pretty code, this link shows how it could be done:
.Net EF Core 2.1 Get DbContext from IQueryable argument
If you are using dependency injection, you could put the instance of DbContext into the container in the scope of the web request or the operation and inject it into the class with the DoMagic method.
Another way could to add the method to the descendant of DbContext class, then the model parametere would not be needed.

ef core new dbcontext executes OnModelCreating only once

we have regularly changing Database Data(every two weeks to once a month).
Usually the latest data has to be used, but in some special cases older data is necessary.
The current info which version has to be used atm is stored in another table.
The Database looks like this, versioned Schema-Names with the same tables beneath it.
YYYYMMDD+Revision
myshema_202001011
table1
myshema_202002011 and so on
table1
myshema_202003011 and so on
table1
I have build a Aspnet core (2.2) service with two DbContext classes,
one for the static schemas that gets the current version to use and one for the changing schemas that accesses those data.
The static DbContext works just fine.
The problem is, even when i use the changing contaxt with a using like,
using (var _context = new ChangingDbContext()){}
the constructors and OnConfiguring are executed each time but the OnModelCreating method is only executed once.
This leads to NOT updating to the current schemas.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
modelBuilder.Entity<my_table>(entity =>
{
entity.HasKey(e => e.key_adr);
entity.ToTable("mytable", $"myshema{mySchemaVersion}");
});
}
Has anyone a clue how to get a "really" new context where OnModelCreating is executed every time?
Or maybe another solution how to handle those changing Schemas?
To continue from my comment. The below db table design allows you or users add as many as new fields to an object as they want. And I think it gives most flexible structure.
Let's assume in a eCommerce system, we provide 3 fields (Name, Code, Price) for the product. But we also allow users want to add their custom fields to their products (e.g. Promotion1Price, Promotion2Price, Discount, ...)
PRODUCT (ProductId, Name, Code, Price)
CUSTOMEFIELD (FieldId, FieldName, FieldType)
PRODUCT_CUSTOMFIELD (ProductId, FieldId, FieldValue)
Let me know if this doesn't serve your purpose right.
Solved by this Answer https://stackoverflow.com/a/41985226/6692289
Quote from Example in case it gets deleted.
Derived DbContext that replaces it's ModelCacheKey (and factory) with
a Custom one.
class MyDbContext : DbContext
{
public MyDbContext(string schema)
{
Schema = schema;
}
public string Schema { get; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
.UseSqlServer("...")
.ReplaceService<IModelCacheKeyFactory, MyModelCacheKeyFactory>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(Schema);
// ...
}
}
The factory that creates the Context with a specific key.
class MyModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
=> new MyModelCacheKey(context);
}
The custom ModelCacheKey per context.
class MyModelCacheKey : ModelCacheKey
{
string _schema;
public MyModelCacheKey(DbContext context)
: base(context)
{
_schema = (context as MyDbContext)?.Schema;
}
protected override bool Equals(ModelCacheKey other)
=> base.Equals(other)
&& (other as MyModelCacheKey)?._schema == _schema;
public override int GetHashCode()
{
var hashCode = base.GetHashCode() * 397;
if (_schema != null)
{
hashCode ^= _schema.GetHashCode();
}
return hashCode;
}
}
And using the Context like.
using (var _myContext = new MyDbContext(_schemaNameToUse)
{
}

Set Value before save

I was wondering if there is any way to set a value to an entity onsave?
Because I'm working on a multi tenant web application and I would like to set the the current tenant ID (through simple DI service).
I tried using HasDefaultValue() in Fluent API, however this will try to convert to a SQL function. So this doesn't work for me.
builder.Entity<Order>( )
.HasQueryFilter(p => p.TenantId == _tenantProvider.GetTenantId())
.Property(p => p.TenantId)
.HasDefaultValue(_tenantProvider.GetTenantId());
Any suggestions are greatly appreciated.
You could override the DbContext.SaveChanges() method and iterate the ChangeTracker entries:
public override int SaveChanges()
{
foreach (var entityEntry in ChangeTracker.Entries()) // Iterate all made changes
{
if (entityEntry.Entity is Order order)
{
if (entityEntry.State == EntityState.Added) // If you want to update TenantId when Order is added
{
order.TenantId = _tenantProvider.GetTenantId();
}
else if (entityEntry.State == EntityState.Modified) // If you want to update TenantId when Order is modified
{
order.TenantId = _tenantProvider.GetTenantId();
}
}
}
return base.SaveChanges();
}
Of course, this needs the tenant provider to be injected into your context.
EF Core value generation on add with custom ValueGenerator
Generates values for properties when an entity is added to a context.
could be utilized to assign TenantId to the new entities. Inside the Next method you could obtain the TenantId from the context (or some service).
Taking your sample, the value generator could be a nested class inside your DbContext like this:
class TenantIdValueGenerator : ValueGenerator<int>
{
public override bool GeneratesTemporaryValues => false;
public override int Next(EntityEntry entry) => GetTenantId(entry.Context);
int GetTenantId(DbContext context) => ((YourDbContext)context)._tenantProvider.GetTenantId();
}
The all you need is to assign the generator to TenantId property using some of the HasValueGenerator fluent API.
The only problem is that by design the value generators are called only if the property does not have explicitly set value (for int property - if the value is 0).
So the better approach it to abstract (and fully control) the TenantId property by removing it from entity models and replacing it with shadow property.
Hence my suggestion is, remove the TenantId from entity classes and call the following method inside your OnModelCreating for each entity that needs TenantId column:
void ConfigureTenant<TEntity>(ModelBuilder modelBuilder) where TEntity : class
{
modelBuilder.Entity<TEntity>(builder =>
{
builder.Property<int>("TenantId")
.HasValueGenerator<TenantIdValueGenerator>();
builder.HasQueryFilter(e => EF.Property<int>(e, "TenantId") == _tenantProvider.GetTenantId());
});
}
If you are using EF Core 5+, then you have the option of using the SavingChanges event. This will allow you to set your custom logic for both SaveChanges and SaveChangesAsync without having to override both methods.
Example:
public MyDbContext(DbContextOptions<MyDbContext> dbContextOptions, ITenantProvider tenantProvider) : base(dbContextOptions)
{
SavingChanges += (sender, args) =>
{
foreach (var orderEntity in ChangeTracker.Entries<Order>())
{
if (orderEntity.State == EntityState.Added)
{
orderEntity.Entity.TenantId = tenantProvider.GetTenantId();
}
}
};
}

Categories