dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.52k stars 3.13k forks source link

Events and interception (aka lifecycle hooks) #626

Open divega opened 9 years ago

divega commented 9 years ago

Done in 2.1

Done in 3.1

Done in 5.0

Done in 6.0

Done in 7.0

Backlog

Note: below is a copy of a very old EF specification and reflects thinking from several years ago. A lot of things aren't valid anymore.

We define EF Core lifecycle hooks as the general feature that enables an application or library to sign up to be invoked or notified whenever certain interesting conditions or actions occur as part of the lifecycle of entities, properties, associations, queries, context instances, and other elements in the Entity Framework stack.

For example:

  1. An application can provide a method that will be invoked automatically whenever an object is about to be saved, or it can subscribe to an event that fires when an object is created and its properties initialized, etc.
  2. A framework extension can register an interceptor that gives it an opportunity to rewrite query expression trees before they get translated by EF. This could be used to validate whether a user has access to specific information or to filter query results based on per DbContext filter (see #6440).
  3. Execute SQL after a DbConnection is opened (to use features such as SQL Server App Role)

The need for lifecycle hooks

We want to enable customers to write business logic that triggers in the different stages of the lifecycle of these objects, following well factored coding patterns. We also want framework writers to be able to use these hooks to extend EF Core in useful ways.

In previous versions of Entity Framework we already exposed a few lifecycle hooks. For instance, we had the AssociationChanged and ObjectStateManagerChanged events since the first version, and the ObjectMaterialized event was added in EF4. Up until EF6.x many of the existing hooks are not exposed in the DbContext API. In EF6 we also added several low level extensibility points in Interception that can be used too as lifecycle hooks.

There is a continuum of capabilities related and overlapping with lifecycle hooks, e.g.:

Name Pri Location Cancel or override Description & sample scenario
QueryExecuting 0 DbContext Yes Query interception, custom query caching.
QueryExecuted 3 DbContext No When Execute happened, before the reader is read. Tracing?
QueryCompleted 3 DbContext No After DbDataReader is closed. Tracing?
EntityStateChanged 0 DbContext No Signals all state changes
EntityStateChanging 3 DbContext ? Undo changes or change proposed values before they are set?
ConnectionProvisioning 2 DbContext Yes Execute additional code to make sure the connection is alive, or do logging
ConnectionReleasing 2 DbContext Yes Cleanup something done during Ensure / StartUsingConnection
ConnectionOpened 2   No More likely for tracing. Since SqlClient has fixed invalid connection pools, then this is lower priority
ConnectionOpening 1 DbContext Yes Slightly simpler to use than Ensure/Start, would not require user to check current state. Could also be used for tracing.
ConnectionClosed 1   No  
ConnectionClosing 1 DbContext Yes Slightly simpler to use than Release/Stop, would not require user to check the initial state. Could also be used for tracing.
OnModelCreating 0 DbContext Yes Tweak model before it is cached.
OnModelCreated 1 DbContext Yes Signal that the model is done and execute some custom code, possibly related to caching logic. . Issue: do we need this for ObjectContext? Issue: if the user is going to implement his own caching, we should have an abstract class or interface for that.
ModelCacheLookup 2 DbContext Yes Implement your own caching logic. Tracing?
ModelCacheHit 2 DbContext Yes Execute additional code when the model is found in the cache. Tracing?
EntityLoading 1 DbContext, DbEntityEntry No After object instance is created but before its properties are initialized. Can be used to reset a flag that will be set in newly created instances but shouldn’t be set during initialization, i.e. for validation.
EntityLoaded 0 DbContext, DbEntityEntry No Can be used to setup anything after an object has been materialized, i.e. event handlers, flags, etc.
CollectionLoading 1 DbContext, DbEntityEntry DbCollectiohnEntry No Can be used to setup anything on a collection after it is created but before it is populated. Issue: Could be used to provide your own collection?
CollectionLoading 1 Context, Entity or Collection No Can be used to setup anything on a collection after it has been created and populated, i.e. listeners for its changed event.
ObjectTypeResolving 1 Context Yes Could be used to specify a different type than the original one, i.e. to implement your own proxy mechanism. It should be per type but could return a Func<T> that returns a new instance and the result could be compiled into materialization delegates.
CollectionTypeResolving 1 Context Yes Something similar to ObjectTypeResolving but for collections. Could be used to replace the default collection type with a custom proxy collection with additional functionality (i.e. paging, fine grained lazy load).
Virtual OnSavingChanges Medium DbContext No Can be used to re-implement SaveChanges but still invoke the existing SavingChanges event
SavedChanges Low Context No Could be used to execute cleanup code after SaveChanges. For instance, to call AcceptChanges on each STE change tracker. It is lower priority because virtual SaveChanges covers most scenarios.
EntityStateChanging Low Context, Entity Yes For an entity instance or type in particular we could avoid putting in the modified state. So even if the properties are read-write, the context ignores changes to this entity. Could be also used to suspend fixup on an entity that is being detached.
EntityStateChanged High Context, Entity No Executes logic after an entity has been put in a certain state. Can be used to setup property values, restore state after the changing event.
PropertyChanging Low Context, Entity Yes Any time a property is about to be changed by the framework or any party, if notification or interception is enabled by the entity type. Should make original and new value available. Should also work for navigation, scalar and complex types properties. Tracing?
PropertyChanged High Context, Entity No Any time a change in a property value change is detected.
PropertyLoading High Context, Entity, Collection Yes Intercepts, overrides de loading of a property. Could be used to support loading of properties using stored procedures.
PropertyLoaded Medium Context, Entity, Collection No Tracing?
Writetable IsLoaded High Context, Entity Yes Allows cancelling the loading of a property.
CollectionChanging Medium Context Yes Any time a collection is about to be changed by the framework or any party, if interception is enabled
CollectionChanged Medium Context, Entity No Any time a change to a collection has been detected.
AssociationChanging Low Context, RelatedEnd Yes Can be used to prevent an association from being changed, or to execute business logic when the association is about to change.
AssociationChanged Medium Context, RelatedEnd No Can be used to execute additional logic after an association is changed, i.e. user can explicitly cascade relationships removals into dependent removals, workaround current databinding shortcomings.
RowValidate , RowValidateAdded, RowValidateModified, RowValidateDeleted Medium Context Yes Storage level version of ObjectValidate. Tracing?
SavingChanges event 0 DbContext ? Currently only available on ObjectContext. Should make trigger OnSavingChanges method protected.

Existing hooks

Name Description & sample scenario
virtual Dispose This can be used to do additional cleanup, i.e. on entity instances.
virtual SaveChanges Can be used to execute additional logic before, after or instead of saving changes.

Some open issues:

  1. Need to prototype some coding patterns and try them.
  2. Is logging and tracing part of this API? It seems that ideally we should have the same level of flexibility for hooking mechanisms with logging and tracing as we end up having with this API.
  3. Second level cache should probably expose its hooks through the same mechanisms.
  4. Should we provide low level query interception points with the same mechanisms, i.e. as command tress and store commands? Should we do the same for CUD store commands? Would need to make sure those work well with caching.
  5. Can we get some level of support for async execution of queries with this hook?
  6. Should we provide enough lifecycle hooks to implement custom fixup logic?
  7. Areas of overlap with other extensibilities: read and write properties in object mapping, proxy type creation (can be imperative vs. event driven), equality and snapshot comparisons for change tracking extensibility.
  8. What about customizing identity resolution?
  9. Is Logging and Tracing part of the lifecycle hooks
  10. Is Query interception part of the lifecycle hooks
  11. Even without query interception we should expose when we are about to execute (imagine a profiling tool that measures how much query compilation costs).
  12. Is ContinueOnConflict part of the lifecycle hooks
  13. How do we improve diagnostics? Can we have OnError
  14. Need to do prioritization, costing and scoping
  15. Should we have a fine grained version of CollectionAdding / CollectionRemoving with support for magic methods on the entities to enable collection patterns? We would need a pattern for Contains checks also.
  16. There is a conversation about splitting AssociationChanged this event into properties and collection changes. However, there should be a way to tell the difference between a scalar property change and a nav prop. Should we make AssociationChanged more accessible and add AssociationChanging? This would provide a way to intercept changes in associations independently of cardinality, constraints, etc.
  17. AssociationChanging would need the entity and collection types to collaborate to avoid changes from being made to the graph.