Create one lazy and one eager DbContext via inheritance? - c#

I am using EFCore 3.1.5 and I have a DbContext I want to be able to use in the same controller or service either lazy or eager. However, it seems like I cannot get it to load lazy properly. Eager seems to work fine.
Whenever I do something as simple as:
var users = await _lazyDbContext
.Users
.Take(10)
.ToListAsync();
each navigational property on each User is null. However, with eager loading, it works fine:
var users = await _dbContext
.Users
.Include(x => x.Contact)
.Take(10)
.ToListAsync();
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<LazyUserContext>((sp, opt) =>
{
var connectionString = "very secret";
opt.UseSqlServer(connectionString, x => x.CommandTimeout(300));
opt.UseLazyLoadingProxies();
});
services.AddDbContext<UserContext>((sp, opt) =>
{
var connectionString = "very secret";
opt.UseSqlServer(connectionString, x => x.CommandTimeout(300));
});
services.AddScoped<IUserContext, UserContext>();
services.AddScoped<ILazyUserContext, LazyUserContext>();
}
UserContext.cs
public interface IUserContext
{
DbSet<User> Users { get; set; }
DbSet<Contact> Contacts { get; set; }
}
public class UserContext : DbContext, IUserContext
{
public UserContext(DbContextOptions<UserContext> options) : base(options) {}
public DbSet<User> Users { get; set; }
public DbSet<Contact> Contacts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Users>(e =>
{
e.HasOne(x => x.Contact).WithOne(x => x.User).HasForeignKey(x => x.ContactId);
}
}
}
LazyUserContext.cs
public interface ILazyUserContext : IContext {}
public class LazyUserContext : UserContext, ILazyUserContext
{
public LazyUserContext(DbContextOptions<UserContext> options) : base(options) {}
}
What could the issue be here? I have tried to IoC both the interface and the class in my controller/service. I have tried with and without the services.AddScoped<>().
All I want is be able to use a lazy dbContext or eager dbContext, where I want to default to the eager one.

The solution
All I want is be able to use a lazy dbContext or eager dbContext
You should be able to just set the configuration in the subclasses:
public class LazyContext : MyContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLazyLoadingProxies();
}
}
public class EagerContext : MyContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
}
}
There are two options of setting db option configuration, via the the constructor (and thus the DI route), or via the class itself. Since this particular setting is class-specific and you don't want to juggle it in the DI registration, it makes sense to rely on the class-specific configuration method.
Why your solution didn't work
The reason your initial approach didn't work is because AddDbContext<T> registers that specific T as your dependency type. From the source:
The AddDbContext extension method registers DbContext types with a scoped lifetime by default.
Note that it registers the context type, not any interfaces/ancestors of that context type.
So when you do this:
services.AddDbContext<LazyUserContext>((sp, opt) =>
{
var connectionString = "very secret";
opt.UseSqlServer(connectionString, x => x.CommandTimeout(300));
opt.UseLazyLoadingProxies();
});
services.AddScoped<ILazyUserContext, LazyUserContext>();
If your class has a dependency of type ILazyUserContext, it only listens to the second registration and flatout ignores the first one.
Only when your class has a dependency of LazyUserContext will you actually get the db context options you specified in the first registration.
Note that you can use this type-specific registration behavior to your advantage when you want to register a default implementation:
// Specific interface => specific type
services.AddScoped<IUserContext, UserContext>();
services.AddScoped<ILazyUserContext, LazyUserContext>();
// General interface => explicitly chosen default type
services.AddScoped<IContext, UserContext>();
This allows you to have some classes that demand to specifically have the eager/lazy loading, and other classes which just take in whatever is the default (which could change over time).

The problem is caused by the fact that both context constructors use (depend on) one and the same options type - DbContextOptions<UserContext>.
AddDbContext<TContext> actually registers two types - the context itself TContext as well as factory for the context options dependency DbContextOptions<TContext>.
So you are registering two options factories (along with configuration action) - DbContextOptions<UserContext> and DbContextOptions<LazyUserContext>. However, as mentioned at the beginning LazyUserContext depends on DbContextOptions<UserContext>, so it's simply instantiated with the first options set-up, i.e. exactly like the other.
This is not specific for lazy loading, but for any scenario which requires different options (different database type/connection string etc.) and is the reason generic class DbContextOptions<TContext> exists.
The solution is to change the LazyUserContext dependency
public LazyUserContext(DbContextOptions<LazyUserContext> options) : base(options) { }
and since this won't compile because the base expects DbContextOptions<UserContext>, add second protected constructor to the base class accepting just DbContextOptions
protected UserContext(DbContextOptions options) : base(options) { }

Related

Adding new DbSet to DbContext when application has started and DbContext created

I have a project, in business it will creates table dynamicaly, its working with netcore3.0 and EF.
When an instance of dbcontext is created after dynamic table is created, I will use Assembly Emit to create a new type of the table, and use OnModelCreating method to add dbsets corresponding to tables.
public class ApplicationDbContext : IdentityDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public virtual DbSet<Book> Books { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
//Use assmbly emit to create dynamic types
var types = CreateDynamicTypes();
foreach (var type in types)
{
builder.Entity(type);
}
base.OnModelCreating(builder);
}
}
But when a table is created after the dbcontext is created, I dont know how to add new dbset yet, because the OnModelCreating only run 1 time.
The question: How do I add new dbsets to an instance of dbcontext after its created?
OnModelCreating run only 1 time (when it first initialized) because of performance overhead.
There is one way, to bypass this, by using "Model Customizer"
First, you need some tweaking in OnConfiguring (you need to override basic implementation)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
var serviceCollection = new ServiceCollection()
.AddEntityFrameworkSqlServer();
serviceCollection = serviceCollection.AddSingleton<IModelCustomizer, YourModelCustomizer>();
var serviceProvider = serviceCollection.BuildServiceProvider();
optionsBuilder
.UseInternalServiceProvider(serviceProvider);
}
And your Customizer should look like
public class YourModelCustomizer : ModelCustomizer
{
public override void Customize(ModelBuilder modelBuilder, DbContext dbContext)
{
base.Customize(modelBuilder, dbContext);
var entityTypeBuilderCart = modelBuilder.Entity<Models.Cart>()
.ToTable("ABC");
entityTypeBuilderCart.Property(a => a.UserId).HasColumnName("XYZ");
entityTypeBuilderCart.Property(a => a.ContractorId).HasColumnName("DFG");
entityTypeBuilderCart.Ignore(a => a.CompanyId);
var entityTypeBuilderCartArticle = modelBuilder.Entity<Models.CartArticle>()
.ToTable("IJK");
entityTypeBuilderCartArticle.Property(a => a.UserId).HasColumnName("QWE");
}
public YourModelCustomizer(ModelCustomizerDependencies dependencies) : base(dependencies)
{
}
}
I hope it will help you.
Be aware that this kind of configuration may cause performance issue.
This code works in EF Core 2.x, in EF 3.x may be some changes, and this code might need some changes.

How to properly configure the `services.AddDbContext` of `ConfigureServices` method

I'm trying to run an .NET Core Web application with EF Core. In order to test the repository I've added an MyDbContext that inherits the EF DbContext and interface IMyDbContext.
public interface IMyDbContext
{
DbSet<MyModel> Models { get; set; }
}
public class MyDbContext : DbContext, IMyDbContext
{
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
{
}
public virtual DbSet<MyModel> Models { get; set; }
}
The context interface is injected to my generic repository:
public class GenericRepository<TEntity> : IGenericRepository<TEntity>
{
private readonly IMyDbContext _context = null;
public GenericRepository(IMyDbContext context)
{
this._context = context;
}
}
When I use this code (without the interface) on startup.cs:
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(...));
I'm getting a run-time error of:
InvalidOperationException: Unable to resolve service for type
'IMyDbContext' while attempting to activate 'GenericRepository`1[MyModel]'
And when using this line of code:
services.AddDbContext<IMyDbContext>(options =>
options.UseSqlServer(...));
I'm getting this compiled time error code of:
Cannot convert lambda expression to type 'ServiceLifetime' because it
is not a delegate type
My question is how to properly configure the services.AddDbContext of ConfigureServices method?
(Is there any changes needed inside Configure method?)
If needed I'm willing to modify the IMyDbContext
Use one of the overloads having 2 generic type arguments, which allow you to specify both the service interface/class you want to register as well as the DbContext derived class implementing it.
For instance:
services.AddDbContext<IMyDbContext, MyDbContext>(options =>
options.UseSqlServer(...));
Just found the answer:
I was missing the adding of the scope between IMyDbContext and MyDbContext.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options => options.UseSqlServer(...));
services.AddScoped<IGenericRepository<MyModel>, GenericRepository<MyModel>>();
services.AddScoped<IMyDbContext, MyDbContext>();
}

Configuring DBContext in the constructor of my base repository class

I have a situation where I need to instantiate my DBContext after my solution has started up. I asked this question which indicated that I could do this with a constructor argument.
It was suggested that I implement as an example this:
var connection = #"Server=(localdb)\mssqllocaldb;Database=JobsLedgerDB;Trusted_Connection=True;ConnectRetryCount=0";
var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
optionsBuilder.UseSqlServer(connection);
using (var context = new BloggingContext(optionsBuilder.Options))
{
// do stuff
}
However I have implemented the repository pattern (for better or worst) and given my changed circumstances - not having a connection string until after the solution has run startup - I need to implement this into the base repository class and I am at a bit of a loss..
Currently I have this:
public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IEntityBase, new()
{
public JobsLedgerAPIContext _context;
#region Properties
public EntityBaseRepository(JobsLedgerAPIContext context)
{
_context = context;
}
#endregion
public virtual IQueryable<T> GetAll()
{
return _context.Set<T>().AsQueryable();
}
public virtual int Count()
{
return _context.Set<T>().Count();
}
......
How do I implement this change both instantiating the DBContext in the constructor (there by bypassing the need to add the context as a service in startup) and then with the wrapping each of the virtual methods with "using" etc
EDIT.. Camilo indicated I had not identified when I have the database name.
The basic situation is that the system starts up (This is an Aurelia SPA project which is irrelevant to this issue) sends the package to the browser which shows a login screen. User logs in.. User is verified via a JWT controller.. Once verified in the controller (using a catalog database that has one table with 3 fields - username, password, database name) I use the database name to create a connection string and then instantiate my DBContext at that point.. so via a constructor.
The answers below need to be modified as the one with the factory answer (promising) has errors as discovered by this question.. Nkosi responded with an great answer to the error.
EDIT 2..
This is a response to the edited question below:
Here is my original Client Repository with :base(context) on the constructor.
using JobsLedger.DATA.Abstract;
using JobsLedger.MODEL.Entities;
namespace JobsLedger.DATA.Repositories
{
public class ClientRepository : EntityBaseRepository<Client>, IClientRepository
{
private new JobsLedgerAPIContext _context;
public ClientRepository(JobsLedgerAPIContext context) : base(context)
{
_context = context;
}
public void RelatedSuburbEntities(Suburb _suburb)
{
_context.Entry(_suburb).Reference<State>(a => a.State).Load();
}
}
}
It has a reference to the base class "context". I am not sure how to modify this given that I believe I still need that ":base(context)" at the end. As well, I have a method in this that accesses _context as well which is part of the constructor...
Further I assume that I can no longer inject the service into the controller but instead new it up once I have secured the connection string and then pass that connection string to service.
Also, Given I have now added a singleton on the startup do I need to remove the original entry? :
services.AddDbContext<JobsLedgerAPIContext>(options => options.
UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly("JobsLedger.API")));
effectively replacing it with my singleton reference as per below:
services.AddSingleton(typeof(IContextFactory<>), typeof(ContextFactory<>));
Edited
The answer has been edited to rectify the mistake spotted and
fixed by Nkosi. Thanks, #Nkosi.
Implement a factory pattern. You can create a factory, call it ContextFactory as below:
First, define the interface. Further modified, removed the connectionString parameter
public interface IContextFactory<T> where T : DbContext
{
T CreateDbContext();
}
Create a factory class that implements this interface (edited as per Nkosi answer). Further modified to inject IHttpContextAccessor
public class ContextFactory<T> : IContextFactory<T> where T : DbContext
{
private readonly HttpContext _httpContext;
public ContextFactory(IHttpContextAccessor contextAccessor)
{
_httpContext = contextAccessor.HttpContext;
}
public T CreateDbContext()
{
// retreive the connectionString from the _httpContext.Items
// this is saved in the controller action method
var connectionString = (string)_httpContext.Items["connection-string"];
var optionsBuilder = new DbContextOptionsBuilder<T>();
optionsBuilder.UseSqlServer(connectionString);
return (T)Activator.CreateInstance(typeof(T), optionsBuilder.Options);
}
}
Then modify your base repository and make the JobsLedgerAPIContext protected. This context is going to be set by the derived class. Further modified to remove the constructor. It will use the parameterless constructor.
public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IEntityBase, new()
{
protected JobsLedgerApiContext Context { get; set; }
public virtual IQueryable<T> GetAll()
{
return Context.Set<T>().AsQueryable();
}
public virtual int Count()
{
return Context.Set<T>().Count();
}
}
Change your derived class to use IContextFactory. Further modified to use the _contextFactory.CreateDbContext() parameter less method
The IClientRepository should have SetContext method defined.
public class ClientRepository : EntityBaseRepository<Client>, IClientRepository
{
private readonly IContextFactory<JobsLedgerApiContext> _contextFactory;
public ClientRepository(IContextFactory<JobsLedgerApiContext> factory)
{
_contextFactory = factory;
}
// this method will set the protected Context property using the context
// created by the factory
public void SetContext()
{
Context = _contextFactory.CreateDbContext();
}
public void RelatedSuburbEntities(Suburb suburb)
{
Context.Entry(suburb).Reference<State>(a => a.State).Load();
}
}
In the controller, that receives IClientRepository instance, you can set the connection in the HttpContext.Items, which will be valid for the request. This value will then be retrieved by the ContextFactory using IHttpContextAccessor. Then you simply call the _repository.SetContext(); method on the repository.
public class HomeController : Controller
{
private readonly IClientRepository _repository;
public HomeController(IClientRepository repository)
{
_repository = repository;
}
public IActionResult Index()
{
// save the connectionString in the HttpContext.Items
HttpContext.Items["connection-string"] = "test-connection";
// set the context
_repository.SetContext();
return View();
}
}
Make sure you register the IContextFactory in ConfigureServices as open generics and Singleton as below, also register the HttpContextAccessor and IClientRepository
services.AddHttpContextAccessor();
services.AddSingleton(typeof(IContextFactory<>), typeof(ContextFactory<>));
services.AddTransient<IClientRepository, ClientRepository>();
You may define your JobsLedgerAPIContext like this:
public class JobsLedgerAPIContext : DbContext
{
// public DbSet<Job> Jobs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Data Source=localhost;Integrated Security=SSPI;Initial Catalog=dotnetcore;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// may need to reflect entity classes and register them here.
base.OnModelCreating(modelBuilder);
}
}

How to use IdentityDbContext

For the last few days, I have been playing around with Asp.net's Identity framework.
I have been able to get the Register and Login working however when I try to extend the functionality to saving data against specific users, I find it is different to how I would normally implement it with stock standard EF.
Normally I would use something like below to save data:
using(var context = myDbContex())
{
context.Add(object);
context.SaveChanges();
}
However, when I try to use this approach after inheriting the IdentityDbContext it is expecting an argument. Is it okay for me to create a default constructor that doesn't take any arguments or should I be passing something in?
My Context currently looks like this:
public class AppContext : IdentityDbContext<ApplicationUser>
{
//I am not really sure why options needs to be specified as an argument
public AppContext(DbContextOptions<AppContext> options)
: base(options)
{
}
public DbSet<ApplicationUser> ApplicationUsers { get; set; }
public DbSet<Xxxxx> Xxxxx { get; set; }
public DbSet<Yyyyy> Yyyyy { get; set; }
public DbSet<Zzzzz> Zzzzz { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
}
}
In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppContext>(options =>
options.UseSqlite("Data Source=App.db"));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<AppContext>()
.AddDefaultTokenProviders();
});
Why is this implementation of the context different to the standard dbContext, and how can I save data using this context?
Thanks
Because of this line
services.AddDbContext<AppContext>(options => options.UseSqlite("DataSource=App.db"));
You need to provide a constructor that has the DbContextOptions as paramter, which has nothing todo with IdentityDbContext.
You have two choices now.
Use dependency injection, that is how you are supposed to use it anyway
public class MyController : Controller
{
private AppContext context;
public MyController(AppContext context)
{
this.context = context;
}
}
Secondly you could register your context differently.
services.AddDbContext<AppContext>();
And apply changes in your context, remove the constructor and override OnConfiguring(DbContextOptionsBuilder optionsBuilder)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=App.db");
}
Now you can use it as you usually would do.
using(var context = new AppContext())
{
// do stuff
}
EDIT:
Not part of the actual question but signin, registration and role managing is handled by these classes, that can be injected when using IdentityDbContext
SignInManager
UserManager
RoleManager

EF Core 2.0.0 Query Filter is Caching TenantId (Updated for 2.0.1+)

I'm building a multi-tenant application, and am running into difficulties with what I think is EF Core caching the tenant id across requests. The only thing that seems to help is constantly rebuilding the application as I sign in and out of tenants.
I thought it may have something to do with the IHttpContextAccessor instance being a singleton, but it can't be scoped, and when I sign in and out without rebuilding I can see the tenant's name change at the top of the page, so it's not the issue.
The only other thing I can think of is that EF Core is doing some sort of query caching. I'm not sure why it would be considering that it's a scoped instance and it should be getting rebuild on every request, unless I'm wrong, which I probably am. I was hoping it would behave like a scoped instance so I could simply inject the tenant id at model build time on each instance.
I'd really appreciate it if someone could point me in the right direction. Here's my current code:
TenantProvider.cs
public sealed class TenantProvider :
ITenantProvider {
private readonly IHttpContextAccessor _accessor;
public TenantProvider(
IHttpContextAccessor accessor) {
_accessor = accessor;
}
public int GetId() {
return _accessor.HttpContext.User.GetTenantId();
}
}
...which is injected into TenantEntityConfigurationBase.cs where I use it to setup a global query filter.
internal abstract class TenantEntityConfigurationBase<TEntity, TKey> :
EntityConfigurationBase<TEntity, TKey>
where TEntity : TenantEntityBase<TKey>
where TKey : IEquatable<TKey> {
protected readonly ITenantProvider TenantProvider;
protected TenantEntityConfigurationBase(
string table,
string schema,
ITenantProvider tenantProvider) :
base(table, schema) {
TenantProvider = tenantProvider;
}
protected override void ConfigureFilters(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureFilters(builder);
builder.HasQueryFilter(
e => e.TenantId == TenantProvider.GetId());
}
protected override void ConfigureRelationships(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureRelationships(builder);
builder.HasOne(
t => t.Tenant).WithMany().HasForeignKey(
k => k.TenantId);
}
}
...which is then inherited by all other tenant entity configurations. Unfortunately it doesn't seem to work as I had planned.
I have verified that the tenant id being returned by the user principal is changing depending on what tenant user is logged in, so that's not the issue. Thanks in advance for any help!
Update
For a solution when using EF Core 2.0.1+, look at the not-accepted answer from me.
Update 2
Also look at Ivan's update for 2.0.1+, it proxies in the filter expression from the DbContext which restores the ability to define it once in a base configuration class. Both solutions have their pros and cons. I've opted for Ivan's again because I just want to leverage my base configurations as much as possible.
Currently (as of EF Core 2.0.0) the dynamic global query filtering is quite limited. It works only if the dynamic part is provided by direct property of the target DbContext derived class (or one of its base DbContext derived classes). Exactly as in the Model-level query filters example from the documentation. Exactly that way - no method calls, no nested property accessors - just property of the context. It's sort of explained in the link:
Note the use of a DbContext instance level property: TenantId. Model-level filters will use the value from the correct context instance. i.e. the one that is executing the query.
To make it work in your scenario, you have to create a base class like this:
public abstract class TenantDbContext : DbContext
{
protected ITenantProvider TenantProvider;
internal int TenantId => TenantProvider.GetId();
}
derive your context class from it and somehow inject the TenantProvider instance into it. Then modify the TenantEntityConfigurationBase class to receive TenantDbContext:
internal abstract class TenantEntityConfigurationBase<TEntity, TKey> :
EntityConfigurationBase<TEntity, TKey>
where TEntity : TenantEntityBase<TKey>
where TKey : IEquatable<TKey> {
protected readonly TenantDbContext Context;
protected TenantEntityConfigurationBase(
string table,
string schema,
TenantDbContext context) :
base(table, schema) {
Context = context;
}
protected override void ConfigureFilters(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureFilters(builder);
builder.HasQueryFilter(
e => e.TenantId == Context.TenantId);
}
protected override void ConfigureRelationships(
EntityTypeBuilder<TEntity> builder) {
base.ConfigureRelationships(builder);
builder.HasOne(
t => t.Tenant).WithMany().HasForeignKey(
k => k.TenantId);
}
}
and everything will work as expected. And remember, the Context variable type must be a DbContext derived class - replacing it with interface won't work.
Update for 2.0.1: As #Smit pointed out in the comments, v2.0.1 removed most of the limitations - now you can use methods and sub properties.
However, it introduced another requirement - the dynamic expression must be rooted at the DbContext.
This requirement breaks the above solution, since the expression root is TenantEntityConfigurationBase<TEntity, TKey> class, and it's not so easy to create such expression outside the DbContext due to lack of compile time support for generating constant expressions.
It could be solved with some low level expression manipulation methods, but the easier in your case would be to move the filter creation in generic instance method of the TenantDbContext and call it from the entity configuration class.
Here are the modifications:
TenantDbContext class:
internal Expression<Func<TEntity, bool>> CreateFilter<TEntity, TKey>()
where TEntity : TenantEntityBase<TKey>
where TKey : IEquatable<TKey>
{
return e => e.TenantId == TenantId;
}
TenantEntityConfigurationBase<TEntity, TKey> class:
builder.HasQueryFilter(Context.CreateFilter<TEntity, TKey>());
Answer for 2.0.1+
So, the day I got it work, EF Core 2.0.1 was released. As soon as I updated, this solution came crashing down. After a very long thread over here, it turned out that it was really a fluke that it was working in 2.0.0.
Officially for 2.0.1 and beyond any query filters that depend on an outside value, like the tenant id in my case, must be defined in the OnModelCreating method and must reference a property on the DbContext. The reason is because on first run of the app or first call into EF all EntityTypeConfiguration classes are processed and their results are cached regardless of how many times the DbContext is instanced.
That's why defining the query filters in the OnModelCreating method works because it's a fresh instance and the filter lives and dies with it.
public class MyDbContext : DbContext {
private readonly ITenantService _tenantService;
private int TenantId => TenantService.GetId();
public DbSet<User> Users { get; set; }
public MyDbContext(
DbContextOptions options,
ITenantService tenantService) {
_tenantService = tenantService;
}
protected override void OnModelCreating(
ModelBuilder modelBuilder) {
modelBuilder.Entity<User>().HasQueryFilter(
u => u.TenantId == TenantId);
}
}
Update: Unfortunately this won't work as expected...
I looked at the SQL log and the function in the lambda expression is not evaluated which will result in a full resultset being returned and then filtered on the client side.
I use the following pattern to be able to externally add filters without having a property on the context itself.
public class QueryFilters
{
internal static IDictionary<Type, List<LambdaExpression>> Filters { get; set; } = new Dictionary<Type, List<LambdaExpression>>();
public static void RegisterQueryFilter<T>(Expression<Func<T, bool>> expression)
{
List<LambdaExpression> list = null;
if (Filters.TryGetValue(typeof(T), out list) == false)
{
list = new List<LambdaExpression>();
Filters.Add(typeof(T), list);
}
list.Add(expression);
}
}
And in my context I add the query filters like so:
public class MyDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
foreach (var type in QueryFilters.Filters.Keys)
foreach (var filter in QueryFilters.Filters[type])
modelBuilder.Entity(type).HasQueryFilter(filter);
}
}
And I register my query filters somewhere else (i.e. in some configuration code) like this:
Func<User, bool> func = i => IncludeSoftDeletedEntities.DisableFilter;
QueryFilters.RegisterQueryFilter<User>(i => func(i) || EF.Property<bool>(i, "IsDeleted") == false);
In this example I'm adding a soft-delete filter which can be disabled using the "global" IncludeSoftDeletedEntities.DisableFilter (which is actually powered by a scope mechanism).
The snags here are that EF.Property cannot be used outside the actual expression, so it needs to be where it is.
Another thing to mention is that we need to encapsulate any logic in a Func to avoid it being "cached".

Categories

Resources