You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This proposes introducing extension points resolved from the app service provider to configure the DbContext similar to how IConfigureOptions works. Currently if a feature builds heavily on the DbContext and needs multiple components to work (interceptors, models, translators and custom services, see examples below) then multiple extension methods have to be called from several locations from user code to configure the feature properly. With this proposal the feature can be authored in such a way that user code only ever needs to call a single method.
Proposed solution
Current state of affairs
The user would need to call multiple methods to add all aspects of the feature to the app:
// User codebuilder.Services.AddDbContext<AppDb>(opts =>{// ...opts.AddFeatureExtensions();// 1});builder.Services.AddFeatureServices<AppDb>();// 2classAppDb:DbContext{publicAppDb(DbContextOptions<AppDb>opts):base(opts){}protectedoverridevoidOnModelCreating(modelBuilder){// ...modelBuilder.AddFeatureModels();// 3}}// 3rd party codepublicstaticIServiceCollectionAddFeatureServices<TContext>(thisIServiceCollectionservices)whereTContext:DbContext{ ...}publicstaticDbContextOptionsBuilderAddFeatureInterceptorsAndTranslators(thisDbContextOptionsBuilderopts){ ...}publicstaticModelBuilderAddFeatureModels(thisModelBuildermodelBuilder){ ...}
Should be minimal considering there would be no behavioral change if the API is not used.
The existing IModelCustomizer could be reused for IModelCustomizers seeing as it is going away (#29533) but that could be a breaking change.
The outbox message should be persisted within the business transaction that trigges it, preferably with little overhead
A background processor should dispatch the message outside the business transaction, preferably as soon as possible
For saving the message the ideal solution is to piggyback the message on the same SaveChanges that persists the rest of the data. This way SaveChanges takes care of the atomic insert and the user does not need to manage a transaction scope. With batch commands this doesn't even need a separate roundtrip to the db.
A message could be added manually (e.g. user calls a feature-provided method) or automatically (e.g. reacting to an entity change). In the latter case the feature needs to register an interceptor to hook SavingChanges and process the change tracker.
The context obviously needs to be made aware of the message model somehow, then the entities can be managed via db.Set<TModel>(). It's the author's decision to expose the model to the user (make entity classes public) or not.
The background processor that fetches and dispatches the outgoing messages can be implemented as a BackgroundService added to the app service provider. A naive implementation would periodically poll the database but this would cause unnecessary delay or excess load. For better reaction time the dispatch should be triggered as the transaction completes and the message becomes visible in the db. This can be detected by hooking SaveChanges and optionally (if there's an outstanding transaction) hook Transaction.TransactionCompleted (for System.Transactions) or register IDbTransactionInterceptor (for context-managed transactions). #20273 would help with this, but it is also possible without.
To summarize the complete feature needs to register the following
new model
interceptors
app services unrelated to the DbContext
Entity history made easy
It's a common requirement in business world to keep a history of changes made to an entity. This is somewhat similar to the outbox pattern with the distinction that it does not dispatch the "message". This would need the following:
new model: for the history records
interceptors: to react to SaveChanges and create the history records as necessary
app services: optionally provide controllers etc to make the history queryable and expose it to the end-user
Tenants
Implementing a tenant-aware context would require the following
model customizations: query filters to filter marked entities based on the current tenant
interceptors: hook SaveChanges to set or validate tenant-specific entity properties
app services: a tenant accessor to determine the current tenant e.g. from a request cookie
Again, a similar set of things to add
Improvements to Microsoft.AspNetCore.Identity.EntityFrameworkCore
Disclaimer: definitely not my call to change anything here.
To use an EF store (AddEntityFrameworkStores<TContext>) you currently have two choices:
Derive from IdentityDbContext
This will add and configure the necessary models and set you up completely. The drawback is you can only use a single base class, multiple features cannot be composed to the context this way. It's also easy to forget calling base.OnModelCreating which can lead to unexpected usage.
With this new API ANC.Id.EF could simply hook into an arbitrary context of the user's choice
Additional notes
Should there be an extension point for configuring the concrete DbContext instance (i.e. to hook DbContext.OnConfiguring)?
Configuring the model from the optionsAction/OnConfiguring method is actually possible right now by replacing the IModelCustomizer service. However this interface is being considered for deprecation (Obsolete IModelCustomizer #29533). Also only a single feature could use it without conflicting with others.
Should probably provide convenience methods that include the app service provider and a design-time flag.
Summary
This proposes introducing extension points resolved from the app service provider to configure the DbContext similar to how
IConfigureOptions
works. Currently if a feature builds heavily on the DbContext and needs multiple components to work (interceptors, models, translators and custom services, see examples below) then multiple extension methods have to be called from several locations from user code to configure the feature properly. With this proposal the feature can be authored in such a way that user code only ever needs to call a single method.Proposed solution
Current state of affairs
The user would need to call multiple methods to add all aspects of the feature to the app:
Proposed API surface
Additional methods/supporting classes may be exposed for convenience.
Simplified user code with the new API
The user only needs a single line of code to start using the feature.
Key implementation points
For brevity I'm only indicating the key points of change and not the related boilerplate.
Configuring the context options is straightforward. Add a few lines here:
efcore/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs
Line 1068 in adf3a13
Configuring the model is a bit more involved. The following service/implementation instance should be added to the internal service provider:
#8710 should be helpful here.
Add as dependency to
ModelSource
and execute the actions here:efcore/src/EFCore/Infrastructure/ModelSource.cs
Line 105 in adf3a13
Risks
Should be minimal considering there would be no behavioral change if the API is not used.
The existing
IModelCustomizer
could be reused forIModelCustomizers
seeing as it is going away (#29533) but that could be a breaking change.Examples
A reusable implementation of the outbox pattern
In microservice world this is a common pattern for implementing reliable data exchange between services. Here's an overview for those unfamiliar with it: https://learn.microsoft.com/en-us/azure/architecture/best-practices/transactional-outbox-cosmos#overview
The pattern has the following key requirements:
For saving the message the ideal solution is to piggyback the message on the same SaveChanges that persists the rest of the data. This way SaveChanges takes care of the atomic insert and the user does not need to manage a transaction scope. With batch commands this doesn't even need a separate roundtrip to the db.
A message could be added manually (e.g. user calls a feature-provided method) or automatically (e.g. reacting to an entity change). In the latter case the feature needs to register an interceptor to hook SavingChanges and process the change tracker.
The context obviously needs to be made aware of the message model somehow, then the entities can be managed via
db.Set<TModel>()
. It's the author's decision to expose the model to the user (make entity classes public) or not.The background processor that fetches and dispatches the outgoing messages can be implemented as a
BackgroundService
added to the app service provider. A naive implementation would periodically poll the database but this would cause unnecessary delay or excess load. For better reaction time the dispatch should be triggered as the transaction completes and the message becomes visible in the db. This can be detected by hooking SaveChanges and optionally (if there's an outstanding transaction) hookTransaction.TransactionCompleted
(forSystem.Transactions
) or registerIDbTransactionInterceptor
(for context-managed transactions). #20273 would help with this, but it is also possible without.To summarize the complete feature needs to register the following
Entity history made easy
It's a common requirement in business world to keep a history of changes made to an entity. This is somewhat similar to the outbox pattern with the distinction that it does not dispatch the "message". This would need the following:
Tenants
Implementing a tenant-aware context would require the following
Again, a similar set of things to add
Improvements to Microsoft.AspNetCore.Identity.EntityFrameworkCore
Disclaimer: definitely not my call to change anything here.
To use an EF store (
AddEntityFrameworkStores<TContext>
) you currently have two choices:IdentityDbContext
This will add and configure the necessary models and set you up completely. The drawback is you can only use a single base class, multiple features cannot be composed to the context this way. It's also easy to forget calling base.OnModelCreating which can lead to unexpected usage.
This is very very very far from trivial, see https://github.com/dotnet/aspnetcore/blob/dfff223c75fb2a19b3a0596a7115960c4e6f5983/src/Identity/EntityFrameworkCore/src/IdentityUserContext.cs#L118
With this new API ANC.Id.EF could simply hook into an arbitrary context of the user's choice
Additional notes
optionsAction
/OnConfiguring
method is actually possible right now by replacing theIModelCustomizer
service. However this interface is being considered for deprecation (Obsolete IModelCustomizer #29533). Also only a single feature could use it without conflicting with others.Possibly related issues
The text was updated successfully, but these errors were encountered: