-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to resolve the IServiceProvider used to lease an instance of a pooled DbContext #23559
Comments
@ajcvickers I don't think #13540 will help in this case since this would enable application registered singleton services to be intermixed with EF Core's internal singleton services. Perhaps a small example will help in demonstrate what I'd like to enable with this issue: var sp = new ServiceCollection()
.AddDbContext<MyDbContext>() // this will register the SaveChangesInterceptor internally
.AddScoped<ICurrentUserService, MySuperComplexCurrentUserService>()
.AddScoped<UserStatsService>()
.BuildServiceProvider();
var scope = sp.CreateScope();
scope.ServiceProvider.GetService<ICurrentUserService>().SetUser(...something local);
var db = scope.ServiceProvider.GetService<MyDbContext>();
db.Orders.Add(new Order(...));
db.SaveChanges();
class MySaveChangesInterceptor : SaveChangesInterceptor {
public MySaveChangesInterceptor(IOwnedServiceProviderAccessor serviceProviderAccessor) { ... }
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
var userStatsService = serviceProviderAccessor.GetServiceProvider().GetService<UserStatsService>();
userStatsService.UpdateCurrentUserStats();
return result;
}
}
class UserStatsService {
public UserSevice(ApplicationDbContext dbContext, ICurrentUserService currentUserService) { ... }
public void UpdateCurrentUserStats() {
var currentUserId = currentUserService.GetUserId(); // This would be a scoped service by itself that knows how to return the userId from lets say http request headers.
var user = dbContext.Users.Find(userId);
user.LastUpdateActivityDate = DateTime.UtcNow;
}
} This example is perhaps a bit superfluous however I have plenty of examples where we have a legit reason to interact with scoped application services. In this case, I need to get my hands on an instance of UserStatsService which needs to come from the same scoped ServiceProvider that was used to resolve the current DbContext instance. I introduced a type in EFCore called: IOwnedServiceProviderAccessor (placeholder name), which can give me that piece of information. Here is an example of a Trigger (which sits on top of SaveChanges interception hence experiencing the same limitations) and its need for having access to the ScopedServiceProvider. One way of doing this, is like so: public static IServiceCollection AddMyDbContext<TContext>(this IServiceCollection serviceCollection)
where TContext : DbContext
{
serviceCollection.TryAdd(ServiceDescriptor.Describe(
serviceType: typeof(TContext),
implementationFactory: serviceProvider => {
var instance = ActivatorUtilities.CreateInstance(serviceProvider, contextType) as DbContext;
instance.GetSevice<IOwnedServiceProviderAccessor>().SetServiceProvider(serviceProvider);
return instance;
},
lifetime: contextLifetime));
serviceCollection.AddDbContext<TContext>();
return serviceCollection;
} Assuming that IOwnedServiceProviderAccessor is a thing, we can implement similar patterns for Pools and Factories, as I've done here. Granted this is far from pretty. Given the above, I dont think hooks as suggested in #17086 is relevant for this issue. |
@koenbeuk Given that example, can you not do something like this: public interface ICurrentUserService
{
void UpdateCurrentUserStats();
}
public class MySuperComplexCurrentUserService : ICurrentUserService
{
private static int _counter;
private readonly int _service;
public MySuperComplexCurrentUserService()
{
_service = ++_counter;
Console.WriteLine($"Created MySuperComplexCurrentUserService {_service} ");
}
public void UpdateCurrentUserStats()
{
Console.WriteLine($"Calling MySuperComplexCurrentUserService service {_service}");
}
}
public class MySaveChangesInterceptor : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
eventData.Context.GetService<ICurrentUserService>().UpdateCurrentUserStats();
return result;
}
}
public class Blog
{
public int Id { get; set; }
}
public class SomeDbContext : DbContext
{
public SomeDbContext(DbContextOptions<SomeDbContext> options) : base(options)
{
}
public DbSet<Blog> Blogs { get; set; }
}
public class Program
{
public static void Main()
{
var sp = new ServiceCollection()
.AddDbContext<SomeDbContext>(b =>
{
b.UseSqlServer(Your.ConnectionString)
.AddInterceptors(new MySaveChangesInterceptor());
})
.AddScoped<ICurrentUserService, MySuperComplexCurrentUserService>()
.BuildServiceProvider();
for (int i = 0; i < 2; i++)
{
Console.WriteLine("Creating scope...");
using (var scope = sp.CreateScope())
{
var context = scope.ServiceProvider.GetService<SomeDbContext>();
Console.WriteLine("Calling SaveChanges...");
context.Add(new Blog());
context.SaveChanges();
Console.WriteLine("Calling SaveChanges...");
context.Add(new Blog());
context.SaveChanges();
}
Console.WriteLine("Scope disposed.");
}
}
} Output
As far as I can tell, this does what you want. |
@ajcvickers Yes! that is working indeed, My original test was probably bugged. Thanks for taking the time and effort in working that one out. So for a scoped DbContext, this is indeed working. For a singleton, Scoped services will obviously not work, however a pooled DbContext is somewhere in a gray area. I understand that its essentially a singleton and that ApplicationServiceProvider refers to the ServiceProvider that owns he actual DbContext, however its consumption is typically that of a scoped service. In other words: When I DI inject a DbContext then it should not matter if its registered as a scoped DbContext or a pooled DbContext (as this may be out of my control). Coming back to my earlier point, If we can capture the ServiceProvider that was used to lease a DbContext and if we were to treat that as the ApplicationServiceProvider then from a DbContext consumer point of view, things should just work regardless how it was registered in the DI container. |
(Especially) with the newly released support for SaveChanges interceptors, we often want to communicate back with our application services. e.g. We may want to implement an audit handler that logs changes to an entity. Interceptors are great for this however since they are not able to directly use the same ServiceProvider that was used to obtain the DbContext, it will have to fall back on trickery, it can use the ApplicationServiceProvider which is already a concept, however this is likely not the ScopedServiceProvider used to resolve the DbContext.
It would be great if we can have a reference to the ServiceProvider used to obtain the instance of the DbContext
I've recently released an extension for EFCore called EntityFrameworkCore.Triggered, in which I added extension methods: AddTriggeredDbContext (with similar alternatives for pooled and factory) that on top of simply calling AddDbContext, also ensured that the current IServiceProvider is stored and retrievable later on. This works consistently well, but is tied to the Triggered projects implementation. I also prefer the simpler method of registration:
services.AddDbContext<FooContext>(options => options.UseTriggers())
as its much more flexible. Unfortunately the latter approach does not offer an easy way of capturing the ServiceProvider used to obtain an instance of a DbContext.Similar approaches that I'm supporting currently:
Non of these are great and it would be much more fitting if we had something like:
DbContext.OwnedApplicationServiceProvider
The text was updated successfully, but these errors were encountered: