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.5k 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.
SidShetye commented 5 years ago

@divega and @ajcvickers : it's been several months since an update so wanted to ask if you have clarity on ObjectMaterialized and SavingChanges hook release dates?

ajcvickers commented 5 years ago

@SidShetye Unfortunately, the schedule for 3.0 is pretty full, and this is not planned for that release--as indicated by being in the "Backlog" milestone. That being said, it's possible something small will get slipped in depending on how things go.

optiks commented 5 years ago

For those that are blocked by A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe, we worked around this by delaying our custom ObjectMaterialized-like event in a few different places.

High-level steps are: 1) For queries that can return multiple results (e.g. Where, Select, ...):

2) For queries that can only return one results (e.g. FirstOrDefault, Single, ...):

3) For lazy loading:

NOTE: You also need to keep track of whether the entity has already had the event raised somewhere, otherwise you could end up raising the event multiple times.

Happy to share an implementation somewhere if people are interested.

WillSullivan commented 5 years ago

@ajcvickers So, let's say someone wanted to rewrite queries (to implement complex filtering not supported by the built in filters), where would you suggest they insert themselves into the process?

There seems to be ample opportunity to do this (for example, the Expression passed to the QueryCompiler looks so tempting, and I'd like to beat the query cache with a baseball bat), but lots of warnings not to, and a corresponding lack of documentation on the internals.

A little guidance would be appreciated. I've been investigating and prototyping rewriting queries for the past month and have come up agonizingly close to a practical solution.

ajcvickers commented 5 years ago

@WillSullivan We don't have any great general documentation here. It's something I would like to improve, but unfortunately resources are limited, especially considering we only have engineering resources to write docs. If you have specific questions, then please file a new issue with those questions and we'll try to answer.

WillSullivan commented 5 years ago

@ajcvickers uh, "So, let's say someone wanted to rewrite queries (to implement complex filtering not supported by the built in filters), where would you suggest they insert themselves into the process?"

smitpatel commented 5 years ago

@WillSullivan - If you want to do before any kind of query execution starts, then override IQueryCompiler. If you want to change only when query is being compiled then override IDatabase.CompileQuery* methods.

weitzhandler commented 5 years ago

I need to run certain operations on the database when it got created. Is there a way to get a notification when it's creation completed (i.e. after calling EnsureCreated created the DB)?

ErikEJ commented 5 years ago

Is this for testing? EnsureCreated should not be used in production.

weitzhandler commented 5 years ago

Thanks for your notice @ErikEJ, my question is also about context.Database.Migrate(). Is there a way to intercept those calls and perform stuff afterwards?

smitpatel commented 5 years ago

@weitzhandler - How about writing custom SQL in migration files? After database is created, the first line executed on server would be first line in up method of very first migration file

weitzhandler commented 5 years ago

@smitpatel @ErikEJ I've opened a new issue.

SidShetye commented 5 years ago

Hi folks - any news on when this will make the release? Been requested since 2014, now 2019. 2019 - 2014 = 5 :(

Igneous01 commented 5 years ago

It's sad that EF still doesn't support this when Hibernate has had this capability for many years now.

If I want to add basic auditing and Guid generation I have to hand roll it myself each time.

This should be something that's possible within EF.

` public class Audit {

@Column(name = "created_on")
private LocalDateTime createdOn;

@Column(name = "created_by")
private String createdBy;

@Column(name = "updated_on")
private LocalDateTime updatedOn;

@Column(name = "updated_by")
private String updatedBy;

@PrePersist
public void prePersist() {
    createdOn = LocalDateTime.now();
    createdBy = LoggedUser.get();
}

@PreUpdate
public void preUpdate() {
    updatedOn = LocalDateTime.now();
    updatedBy = LoggedUser.get();
}

//Getters and setters omitted for brevity

} `

mguinness commented 5 years ago

@Igneous01 You can already achieve this in EF Core - see Generate tracking columns for more details.

jakenuts commented 5 years ago

SaveChanges seems like a great place to be able to intercept/extend a context (auditing, caching events) and now with DbContextPooling overriding those methods to call services is less manageable as they can't be passed in to the constructor. A BeforeSaveChanges(Async), AfterSaveChanges(Async) and ErrorSavingChanges(Async) event would be awesome.

ajcvickers commented 5 years ago

@jakenuts A couple of points:

sjb-sjb commented 5 years ago

I would support the events mentioned by @jakenuts. I implemented very similar events in a wrapper class as a way to update entity view models.

A very handy function to complement SaveChanges would be RevertChanges, with somewhat similar events associated with it. One typically wants to revert changes when a user cancellation occurs.

sjb

ajcvickers commented 5 years ago

@sjb-sjb Not saying those aren't useful events. They are still on the table.

SidShetye commented 5 years ago

Hi Arthur and Diego - can we raise the priority of the lifecycle hooks? The community demand has been very strong (30+ participants, 50+ thumbs, over 5+ years) but this item has been waiting (waiting, waiting ...) in the queue to be punted across every release. I understand the team has a challenging schedule but perhaps a swap/re-prioritization against another issue/feature would help get this long-waiting item out of the queue? Thanks

CC: @ajcvickers @divega

EDIT: More precisely, the worry is this isn't on the 3.0.0 milestone or on any plan in general.

ajcvickers commented 5 years ago

@SidShetye Unfortunately, at this point there isn't anything that can be bumped to give this a higher priority.

douglasg14b commented 5 years ago

Hell, lifecycle hooks have been a community want since EF v4 in 2010.

SidShetye commented 5 years ago

@ajcvickers That is disappointing - and hard to believe. Why? Because looking at the 474 issues open in 3.0.0 milestone, there is only ONE. solitary. single. issue with more customer demand than this issue. Every one of the remaining 473 issues has less customer expressed demand or importance (!).

I mean your customers are literally telling you "this issue is more desired than the other 99.78% of the issues". And yet the product road-map simply tosses this aside? Even with noisy measurements, the balance is overwhelmingly off.

I believe product development should be rationally driven by the customer and markets. But this issue gives the impression that it's highly whimsical. Like: "Yeah, you customers can go take a hike, we're going to build whatever we want." On top of it, this is also nothing new (since you said "at this point") - we've been at it since 2014!

We strive to work closely but this is a disappointing project dynamic.

CC: @divega

divega commented 5 years ago

@SidShetye Customer feedback is very important to us. But there are several other sources of information we use to decide what goes into each release. By the way, we made an attempt to describe the process at https://docs.microsoft.com/ef/core/what-is-new/roadmap#release-planning-process.

Besides that, this isn't really a regular issue. It is more of an "umbrella" issue that represents several individual features. Each lifecycle hook clearly should have its own priority, but because we haven't broken it down into individual issues (something I would like to do, but I haven't found the time for), we get lots of votes here.

We actually implemented some of the features that used to be tracked here (state change events) in EF Core 2.1.

You are welcome to comment here about what specific lifecycle hooks would be most valuable for you and why.

You can offer to help by contributing some of them.

You are also welcome to complain about the fact that your favorite features aren't implemented and all the customers that are tracking this issue will see it. But let me set the expectation that it is very late in the 3.0 release, and given the number of work items we have in progress and the resources we have available, it is very unlikely that we will be adding any more enhancements to the list of this release.

SidShetye commented 5 years ago

@divega @ajcvickers - believe it or not, we're huge fans of the EF team and respect the work coming out right from EFs early days. So much so, that at the unique intersection of ORMs and Data Security where our Crypteron products, services and IP reside, we've heavily invested and have always treated EF (till EF 6.x) as our 'golden target' that drive the designs for other ORMs we support like Java's JPA/Hibernate. We're also promoting enterprise migration to the cloud/Azure by reducing data security concerns on those workloads - these are common customers between your team and us. In fact we're an Azure Security Partner working on an even closer commercial integration. The synergies are great and we're EF proponents in general.

Mapping to some of the guiding questions on your roadmap planning process:

In terms of what lifecycle hooks are desired, why etc., we discussed that here last year. I hear you about challenges with time on 3.0.0. You guys know best about internal resource allocation, but perhaps 3.0.1 :) ?

Thanks sid(at)crypteron.com

AFDevMike commented 5 years ago

I very much appreciate the EF teams work and am hesitant to step into what is best described as a passionate back and forth. I have one request however.

Objectively, I would assert if this umbrella issue has the 2nd most customer demand, but can't be treated as a single feature request. Its worthy of the time to break into individual issues to track demand more accurately.

divega commented 5 years ago

@SidShetye thanks for the pointer to https://github.com/aspnet/EntityFrameworkCore/issues/626#issuecomment-359082350. To give you some perspective, there were ~90 comments before that, and ~70 comments after that in this issue. That is one of the reasons it has become extremely hard to distill the comments in this issue into actionable work items that we can prioritize. I would be very grateful if you go ahead and create a new issue for the specific hooks you need. Please, try to be very specific about what you need (unfortunately, after re-reading that comment a couple of times, I am still not sure), and feel free to link to it from here.

@AFDevMike

I would assert if this umbrella issue has the 2nd most customer demand

I wonder how you are counting. In https://github.com/aspnet/EntityFrameworkCore/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-desc this appears to be in the 14th position. But as I said, the main problem is that it is impossible hard to distill what it really means in terms of actionable work items for the individual lifecycle hooks and their relative priorities.

AFDevMike commented 5 years ago

@divega I was counting based upon @SidShetye's assertion here: https://github.com/aspnet/EntityFrameworkCore/issues/626#issuecomment-496730616

Because looking at the 474 issues open in 3.0.0 milestone, there is only ONE. solitary. single. issue with more customer demand than this issue. Every one of the remaining 473 issues has less customer expressed demand or importance (!).

And since it wasn't refuted in your direct response, I assumed it to approximate the reality. My error, apologies for noise.

weitzhandler commented 5 years ago

@divega

I opened a new issue that is related to life-cycle hooks. In my scenario, I need to have a post-OnConfiguration hook, so I can call Database.Migrate. See #15846.

ajcvickers commented 5 years ago

See #15066 for IDbCommandInterceptor (for command interception), for which there is now a PR: #16113

SidShetye commented 4 years ago

For folks tracking this directly or coming from search (SEO!) - the actual work items are in #15910 and #15911. Hoping to see this released in EF Core 3.1 🤞 !

RobK410 commented 4 years ago

We need interception to rewrite the SQL before execution because there is no support in EF for FULLTEXT functions nor XML XPath query syntax. We dynamically build an IQueryable based on an ad hoc query builder interface in our application, and that turns our XML data typed model properties into column LIKE '%value%' statements in the SQL.

Interceptors were the ONLY work around we could come up with to parse the SQL, replacing the LIKE '%value%' with CONTAINS(fulltextcolumn, 'value') and xmlcolumn.query('xpath value') = 1

Where's the common work around described in this opened issue for manipulating the SQL before execution?

I'd personally like to see full XML and FULLTEXT syntax support. Is there a issue tracked for those? Can the community code and submit these changes?

ajcvickers commented 4 years ago

@vasont For full-text search see #10488, #11481, #11486. For the low-level hook to modify SQL, database interceptors is the way to do this.

RobK410 commented 4 years ago

@vasont For full-text search see #10488, #11481, #11486. For the low-level hook to modify SQL, database interceptors is the way to do this.

@ajcvickers Thanks so much for the references. A project this big can get unruly trying to find the right info with different search terms.

Awesome news on the FreeText search functions. It would be more awesome to see similar for the XML query functions. :)

douglasg14b commented 4 years ago

@vasont I'm happy I'm not alone in finding information discovery challenging with large projects like this!

@ajcvickers Which is why I (and I'm sure many others) greatly appreciate this repos openness to question issues. As opposed to offloading those to a gitter or chat where the information is rapidly lost to time. It makes it possible to find conversations and solutions to niche issues that would otherwise be neigh-impossible to locate or discover, and gives way for some niche discussions to turn into features.

Nefcanto commented 4 years ago

Is this still not created in EF Core?

mguinness commented 4 years ago

@nefcanto Take a look at interception of database operations in New features in Entity Framework Core 3.0, but lifecycle hooks didn't make it into 3.0 release.

JValck commented 4 years ago

For those who came here via SEO: because I needed this functionality myself, I created my own package with lifecycle hooks.

You can download it via NuGet. Full docs are available on the Github page of the project.

jakenuts commented 4 years ago

I was just wondering how I might detect changes to a relationship collection to update the owner and it seems like this is the same issue. You've built a great system for loading and persisting data but between those two actions there is "change" and it is only addressed in terms of the first two. When I modify a property, add an item to a collection I want an easy method for hooking into that process and reacting. We've probably all written some complicated method of using change tracking to do tiny little tasks like setting an UpdatedDate and that just promotes brittle redundant code across all of our projects. Prioritize change hooks, please.

sjb-sjb commented 4 years ago

@jakenuts you can use notification tracking entities with a collection that implements INotifyPropertyChanged/Changing, see https://blog.oneunicorn.com/2016/11/16/notification-entities-in-ef-core-1-1 You can also use a collection type that implements INotifyCollectionChanged.

In this approach, when you assign to a principal entity reference and both entities are attached to the same context, EF will immediately add the dependent entity to the parent collection resulting in a CollectionChanged from the collection.

Watch out for the fact that this only happens when both entities are attached to the same context. In my work I attach the entities if they are not already attached.

You can also expect to get PropertyChanged notifications for database-assigned primary keys when they are set during a SaveChanges.

chester89 commented 4 years ago

We also need to propagate changes to several systems. I looked at @JValck solution, found it too fine-grained for our purposes. At the end I implemented simple publish/subscribe by overriding DbContext.SaveChangesAsync method. It would be great to have such mechanism in the library itself, because IDbCommandInterceptor API is too low-level

markusschaber commented 4 years ago

Propagating changes in a transaction safe way is not easily possible - the service might crash in the wrong moment, leaving an inconsistency between the propagated changes and the real database contents.

We're thinking about using PostgreSQL LISTEN and NOTIFY mechanism for this problem.

optiks commented 4 years ago

Propagating changes in a transaction safe way is not easily possible - the service might crash in the wrong moment, leaving an inconsistency between the propagated changes and the real database contents.

We're thinking about using PostgreSQL LISTEN and NOTIFY mechanism for this problem.

I’d suggest you’d be better going with with one:

chester89 commented 4 years ago

Propagating changes in a transaction safe way is not easily possible - the service might crash in the wrong moment, leaving an inconsistency between the propagated changes and the real database contents. We're thinking about using PostgreSQL LISTEN and NOTIFY mechanism for this problem.

I’d suggest you’d be better going with with one:

DTC is not supported in .NET Core https://github.com/dotnet/runtime/issues/715

markusschaber commented 4 years ago

@optiks LISTEN and NOTIFY in PostgreSQL are integrated with the database transactions, so what's the reason you advise against them?

Publish with later polling puts a high load if you have lots of data, and only small amounts change. Also, you increase latency by the polling interval.

We'll have a closer look at NServiceBus Outbox. But at first glance, it just looks like reinventing a transactional messaging system in the database by storing message in tables, so why not just use the one provided by the database?

ajcvickers commented 2 years ago

See https://github.com/dotnet/efcore/issues/27093

Evengard commented 1 year ago

Is there a OnModelCreating override implemented, or smth along this issue: https://github.com/dotnet/efcore/issues/9330 ?

ajcvickers commented 1 year ago

@Evengard I opened #31206.

xaviergxf commented 3 weeks ago

is there any interceptor available that allow audit feature when using ExecuteUpdate, ExecuteDelete?

roji commented 3 weeks ago

@xaviergxf command interceptors should work for ExecuteUpdate/Delete just like for any other EF database operation.