Add an interception point for navigation property fix-up
Question
I'm trying to figure out how to access navigation properties when data is loaded. I've tried IMaterializationInterceptor.InitializedInstance and ChangeTracker.Tracked. Neither executes after applying the navigation properties. Similar questions have been asked, but I don't see any solutions that work:
https://github.com/dotnet/efcore/issues/32535 https://github.com/dotnet/efcore/issues/35386
In https://github.com/dotnet/efcore/issues/32535, there's a suggestion for interception after the result set has been consumed. Apparently it's described somewhere at https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#database-interception. But they all look like intercepts that happen prior to properties being set on the objects, so that does no good.
For some context, my objects implement INotifyPropertyChanged and I'm simply trying to wire child objects to their parents, so that the parents know when a child property has changed. I suppose everywhere that I retrieve data, I could execute code to traverse the object graph and do the wiring, but that seems like a bad way to do it.
Your code
Stack traces
Verbose output
EF Core version
9.0.3
Database provider
Microsoft.EntityFrameworkCore.SqlServer
Target framework
.NET 9.0
Operating system
Windows 10
IDE
Visual Studio Enterprise 2022 17.13.6
@mokarchi the above looks like AI-generated responses; if so, please don't post these on issues. If users want AI answers, they can do them ourselves, and such answers may contain hallucinations etc.
@mokarchi the above looks like AI-generated responses; if so, please don't post these on issues. If users want AI answers, they can do them ourselves, and such answers may contain hallucinations etc.
You're absolutely right. concerns about the accuracy of AI-generated responses are totally valid. In this case, the content was carefully reviewed and based on source code, with AI used only to assist in wording. That said, I understand it's not preferred in this context, so I’ve removed the comment. Thanks for the feedback.
Thanks @mokarchi. It seems like your examples are mapping the navigation properties before EF has a chance to do it and at that point I can wire up my eventss? Seems like a lot of the suggestions are more work than they should be. Below is what I'm currently doing and is fairly easy. I think it's what was meant by your Custom Method suggestion.
DatabaseContext.Buildings.Include(p => p.Floors)
.ThenInclude(p => p.FloorPlans)
.ToList()
.InitializePropertyChangedEventHandlers();
in conjunction with this extension method:
public static List<T> InitializePropertyChangedEventHandlers<T>(
this List<T> items) where T : BaseBusinessObject
{
foreach (var item in items)
{
item.InitializePropertyChangedEventHandlers();
}
return items;
}
The obvious problem is I have to make this method call everywhere there is a select. I guess it's not a huge problem, but it just seems like there should be a better way.
@uler3161 If you'd like to avoid this repetitive call implement a custom interceptor and register it with the DbContext.
@uler3161 If you'd like to avoid this repetitive call implement a custom interceptor and register it with the DbContext.
As mentioned in my original post, it appears to me all of the interceptors execute prior to navigation properties being set. And really, if I'm reading the docs right, not only have navigation properties not been set yet, I don't think any entities have been mapped yet. It seems to me IDbCommandInterceptor.ReaderExecuted is the latest interceptor to run when selecting entities and I don't see where it provides any entities.
Edit: I forgot IMaterializationInterceptor is also an interceptor. Entities are available with this, but navigation properties are not set.
you can combine it with a post-materialization step to initialize event handlers:
public class CustomMaterializationInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
{
if (entity is BaseBusinessObject baseEntity)
{
baseEntity.InitializePropertyChangedEventHandlers();
}
return entity;
}
}
you can combine it with a post-materialization step to initialize event handlers:
public class CustomMaterializationInterceptor : IMaterializationInterceptor { public object InitializedInstance(MaterializationInterceptionData materializationData, object entity) { if (entity is BaseBusinessObject baseEntity) { baseEntity.InitializePropertyChangedEventHandlers(); } return entity; } }
Once again, as mentioned in the original post, I've tried this and it does not work. My InitializePropertyChangedEventHandlers method requires navigation properties to have been set so that the parent can wire up PropertyChanged events in the children.
EF Core doesn’t have a built-in way to handle this, and the EF team currently has no plans to support it either. But if you really need it, you can do it manually. After loading your data with Include, you can define a method like WireChildren() to hook up the child objects to their parent.
It works fine and can be manageable for small to medium projects, but I wouldn’t recommend it unless you really have to — mainly because you’ll need to call that method yourself every time you load the data. In bigger projects or when there are lots of queries, it can easily become a hassle.
@mokarchi
You're absolutely right. concerns about the accuracy of AI-generated responses are totally valid. In this case, the content was carefully reviewed and based on source code, with AI used only to assist in wording. That said, I understand it's not preferred in this context, so I’ve removed the comment. Thanks for the feedback.
OK - if you've actually investigated and only used AI to assist in wording that's totally valid then 👍 Thanks for helping out!
I'm working on a use case where all entities returned from the context have a computed value on them which is dependant on any loaded navigations. So having IMaterializationInterceptor be able to intercept the fully loaded record would be perfect for that. Otherwise I have to find every single place (in an enterprise codebase!) where I query the database context and call a method to compute the value, which is obviously, hideously unmaintainable.
I figured out a way to do it. It's not pretty but you can use ReplaceService to replace IAsyncQueryProvider with a class that derives from EntityQueryProvider and override the three execute methods (two synchronous, one asynchronous), from there you can invoke the base method and the result will be the complete record (or list of records) with all navigations included. The async override is a bit tricky, because you need to generate a wrapping Task or IAsyncEnumerable to ensure your handling code is awaited, but I'll show how to do that when I post my full solution later. In any case this is just a workaround (and it depends on code that raises EF1001, so it's probably not wise to bet on it being there forever). It would be great to have an official solution.
you can combine it with a post-materialization step to initialize event handlers:
Actually, no, no you cannot I just ran into just such a scenario, in which I must navigate my entity instances up the child-parent tree. The ParentId is there, but the Parent has not yet landed at this stage.
Or, at best, would quantify and qualify that as, "it depends".
If your clerical actions are confined to the instance itself, it is fine. But when it involves traversing the tree at all, not so much.
baseEntity.InitializePropertyChangedEventHandlers();
Honestly though to the OP root concerns... I would not do this as a matter of EF hydration. It is something that I think should be handled internally of your business entity classes, IMHO. Speaking from personal experience there, we lean into ObservableObject for instance, in the MVVM package, and have had pretty good success with it to this point.
HTH, good luck.
Sorry I haven't gotten back to this. I've thrown together a little tutorial on my workaround here: https://gist.github.com/AceCoderLaura/31120087388a3df78bc731ee9b659a02
Hope this helps anyone with a similar use case.
Hope this helps anyone with a similar use case...
@AceCoderLaura Curious myself, is there a sequence diagram including when ISingletonInterface is triggered, maybe a UML sequence diagram, from ORM or transaction ?
At present, I've taken to IQueryExpressionInterceptor myself, to help coordinate domain entity trigger timing, but if there is a better one... Bit tricky involving QEI since it involves injecting some Expression code, we introduce a LINQ .Select(...) clause for example.
Curious what ISingletonInterface does in contrast, is that operating over the entire graph, potentially, of materialized or hydrated instances; there's a difference. Materialization is too early, is typically just following construction. We can work with mid-flight during Expression<...> evaluation. Post- all of that, even better, when the collection and/or instances are all but disconnected, in our case by design, we want the entities disconnected, release the repository (context, sets) and carry forward with the app, in our case.
If we're talking about materialization which I think may involve ISingletonInterceptor, i.e. interface IMaterializationInterceptor : ISingletonInterceptor { } then the results may not be dependable, I was finding, at least, that parent-child inverse bidirectional or navigational properties had not yet been connected. Segue to IQueryable, Expressions and so forth.
Anywho FWIW YMMV (your mileage may vary) depending on your use case.
@mwpowellhtx ISingletonInterceptor is never triggered, it's an empty interface. It's just a base interface for interceptors with singleton lifetime. The reason I put together my workaround is precisely because IMaterializationInterceptor was insufficient and is invoked before navigations are populated. The IResultInterceptor (not shipped with ef core) is a custom interceptor that is called when the result of a query is enumerated/awaited; this only happens because the EntityQueryProviderInterceptLayer (again, not part of ef core) injects it into the enumerable/task before the task/enumerable is returned. The IResultInterceptor implementation receives what the query caller receives (i.e. the final, populated record), but it doesn't operate over the entire graph (in my use case I traverse each record using reflection to reach other records loaded via navigations, this probably results in overlap when identity resolution is enabled, but it's better than nothing). If I get more time I might update the gist with a more detailed explanation.
IQueryExpressionInterceptor looks promising, I don't recall trying it when I was putting my workaround together but I might look into it. Seems like I might be able to do something similar with it.