-
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
Ephemeral/Shadow navigation properties or other mechanism of automatic joins. #32658
Comments
@voroninp I've read through this and I think it's extremely unlikely that we will implement any of this. |
@ajcvickers But do you at least agree that it's a valid concern/pain point? |
@voroninp Not really, no. |
@ajcvickers Ok, how would you work with the aggregates on the read-side of the application?
|
I'm struggling to understand your argument above. Modeling e.g. blogs and posts via types that reference each other is the standard, natural way to do things in .NET (regardless of databases or ORMs); if I want to pass around a list of blogs - including their posts - each blog must reference their post - or I have to start passing around an additional dictionary just to represent the mapping between blogs and posts. I don't know of any design principle that would mandate shredding an object graph like this, or any .NET developer who would prefer to actually work in this totally disconnected way. Given that, EF generally strives to support modeling your database via regular .NET types, with the same design principles and relationships you'd use in .NET. Throwing all of that out just because of the occasional difficulty of knowing which entities were included which weren't simply doesn't make sense to me.
EF has its normal/standard way of doing things - that's navigation properties; I recognize that there can be issues with the concept in some scenarios, i.e. needing to know/check whether some navigation property was loaded or not. At the same time, the concept is incredibly useful for passing a graph around - a blog with all of its posts, as a single unit - in application business logic; this is something that we know very, very clearly from our users. At the end of the day, I'm happy to have conceptual discussions about how EF works and how it could work; and I appreciate your attempts to sometimes push EF to the edge and use it in atypical, novel ways. But at a very concrete level, EF (like any other component) has its way of doing things; those obviously cannot accomodate the exact tastes and needs of every user out there, and so in some cases users have to adapt their coding style to how the product works; this is one of these cases. So you'll have to make do with either regular navigations, or with manual joining on foreign keys. |
One more note; the only way your proposal can actually help with the problem your describing, is to force the developer to always load the posts at the point where they're needed - this is something that's very bad for performance. Assume, when thinking about this problem, that eagerly loading the blogs with their posts is vital for performance, e.g. to avoid doing an additional roundtrip for each and every dependent. |
That's absolutely true with one particularly important nuance:
If I return you such an aggregate with optional (nullable) one to one relationship, how would you know whether it was not included, or that optional dependency does not exist at all? |
I doubt these two types of entities should be changed within a single transaction. They are two different aggregates. |
I had some check to prevent access to Navigation properties withing aggregates:
with checks in my repository:
And analyzers ensuring that these properties are never accessed by entities themselves. IDK, maybe @vkhorikov could explain the idea better =) |
You make the fact that the navigation was included part of the contract for the method in question. In other words, you mandate that the graph passed to a particular method must have its e.g. posts populated, and not doing so is a programmer error.
I'm not talking about transactions - I just need to load blogs and posts efficiently in order to show them to the user. The concept of an aggregate does not forbid having references outside of the aggregate. |
I'd really examine exactly what it is you understand by the concept of aggregate here... You seem to be making some assumptions - or working with a definition - that doesn't really correpond to what I understand by the concept. |
It depends on what you mean by reference. Foreign Key is also a reference. But if you mean reference to the instance of other aggregate, then it's forbidden. |
I mean a .NET reference (e.g. between Blog and Post), with conceptually represents the same thing as a foreign key in the database. Can you point to a resource explaining why it would be forbidden to have a .NET reference between a Blog CLR type and a Post CLR type? |
I'll mention it once again. There are very different scenarios:
People tend to use aggregates (if they even use reach model) both for commands and queries. EF does not provide a straightforward way to define both read and write models simultaneously. Table splitting may be helpful, but it does not prevent changes. |
Even if I accept the idea that there shouldn't be cross-aggregate references when doing writing; given that in EF you have just one model for both reading and writing - using the same CLR type and properties - not having navigation properties for the purposes of writing would mean no navigation properties for the purpose of reading, which is a non-starter. Or am I missing something? That would lead in the direction of having a different type model - different CLR types - for reading and writing, just so that you can omit the navigations from the write side. I'm following this train of thought out of theoretical interest, but this obviously doesn't seem like something anyone would want. It would also mean that you can no longer query for some entities, modify them and then persist back to the database, since the read types and the write types are different; so you'd need to transform the queried read hierarchy (which has navigation - so that you can load related entities, pass them around), into a write hierarchy (where there are no navigations). This basically kills the UoW pattern, at least in the simple way it's currently done, and makes things inefficient, since hierarchies have to be constantly copied/transformed from read to write. Is this really a direction you're saying we should consider? Or have I misunderstood things? Specifically on the article you cite above:
This seems to be the crux of the matter; this is an attempt to remove the possibility of modifying multiple aggregates in a single transaction, by removing the navigation between them. Several thoughts on this:
At least in EF, references are already never eagerly loaded; you need to explicitly use Include to d that. So it doesn't make sense to me to remove navigation properties just to avoid eager loading - the article possibly has other data layers in mind, or something else. More generally, applying general DDD/theoretical concepts to EF isn't a simple or straightforward thing; EF is a specific ORM with its features and ways of doing things, and not everything is immediately relevant. I'd be wary of just reading DDD articles and trying to apply them verbatim with EF without careful consideration. |
Yes, the feature request is exactly to have navigations without having them as properties :-)
Ideally yes, I'd love to have that opportunity.
Read types - is what exposed to the UI. In other words, there are requests which ask for data, and there are some requests which change data. Two separate flows. For the read-models it's different, you just fetch it, but never change or persist. I mean read and write not relative to EF.
You are right, and I had to introduce a check for this. ;-)
For aggregates I use |
Can you imagine what it would look like to actually work with EF in such a world of two models? Can you post an example snippet of a typical unit-of-work, where a graph of some blogs and their posts are loaded (in a single query), modified, and then persisted back? I can't imagine this being anything that anybody actually wants to do - just in order to be defensive against accidentally modifying two aggregates in a transaction.
Sure, but my point was that in EF specifically it makes little sense to remove non-owned navigations just to prevent them from being eagerly loaded, since eager loading is explicit anyway (via Include). The way I'm understanding the article, it's recommending removing the navigation for situations where having the navigations implies that they're automatically loaded; but this isn't the case in EF - you need to explicitly opt into loading navigations, which means there's no performance advantage in removing the navigations. |
This is probably my English. But what you offer is exactly what I need to avoid.
|
The idea is that you define a fixed graph (root entity and all includes) which contains just enough data to work with business invariants while processing the transaction. The challenge of designing an aggregate is defining the properly sized graph. |
Thanks for the code sample, that helps (as always). So you're not proposing a different model for reading (with navigations) vs. for writing (without navigations) as I wrote above - you're just proposing having a single model for reading/writing, and dropping navigations altogether, forcing yourself to use explicit Joins in queries everywhere. First, in your GetBlogOverview you've chosen a scenario where you only need the count and max date of a blog's posts (which you then just represent in an anonymous type). What happens when you need to return each blog's actual post instances, because e.g. the UI needs to show all of them? How do you represent the relationships between the blogs and the posts given that there are no navigation properties, and how does consuming code (e.g. in the UI) access them? |
First, it's important to understand what actual data UI needs. Is it just post tile, or post title plus first several lines of the post? Or maybe the number of new comments since last visit. If I could predefine read-model (kind of virtual view) I could ask db context for a set of these models: But if we have only aggregates which do not have explicit navigation properties, then it will be a simple projection:
|
Let's assume that the entire Blog and Post entity types are needed (e.g. bound to some UI form), so a PostPreview doesn't really make sense. You're basically introducing wrappers all along the way - BlogWithPosts - to work around the fact that you've removed the reference from Blog to its Posts. Now try imagining what you'd need to do in order to use EF's change tracking to persist arbitrary changes in the graph back to the database. Since instead of having a simple Blog referencing its Posts - which EF knows about, tracks, and can perform automatic change tracking on, you'll now have to do quite a bit of extra manual work. For example, imagine that the user moves some Post from Blog1 to Blog2; when using navigations, EF detects that and does everything automatically - how much manual work will you have to do here in order to support that? I'll skip ahead to where I'm trying to go... Do you really think that all the added complexity here - the extra wrappers, the manual joining via foreign keys, etc. etc - are worth the value of defensively "squelching the temptation to modify multiple aggregates", as quoted in the article? Or are we just following a recommendation which might make sense for some other ORM (not sure), but very, very significantly increases complexity here? I'd argue that the risk of bugs you're introduced with this convoluted pattern far outweigh the risk of an accidental transaction including two aggregates. |
Situation complicates a lot if aggregate becomes more complex. |
I'm sorry, I'm not seeing the relationship between the last comment and our previous discussion... |
You should not track read models. Only aggregates.
It's a very good question. If we are speaking about DDD we are doing more than just CRUD.
It's even questionable whether you should have a FK constraint between Blog and Post. Business rules may require you to let blog be removed, letting posts stay (No restrict, and no Cascade delete). Posts just become orphaned. Post is an aggregate. We are ok, if it does not reference a blog. Here we do not check whether blog with |
Imagine entity where all the mapping is applied to private fields. No imagine how hard it will be to use the entity for the queries. You'll be forced to use |
That can just be an optional foreign key (optional navigation); the fact that a post can exist without a blog is not a reason to not have a foreign key/navigation. In any case, this has been an interesting conversation, and I learned some things from it. I'm not very knowledgeable about DDD, but I'm generally skeptical of the patterns being recommended, and the extreme defensiveness; all that seems to add a very large amount of complexity - as well as depart from very natural, idiomatic ways of doing things in .NET - for very little actual return value. But at the end of the day, the request here seems to be to allow mapping some parameter-les method call on an entity type to a navigation: ctx.Set<Blog>().Where(b => b.Posts().Any(p => ...)); Here, |
Exactly |
OK, I'll discuss with the team (though there's very little chance we'll actually be able to work on something like this any time soon). For my own understanding, can you point me to a resource explaining why it's better for the navigation to be accessed via a method call |
There's no such resource. The idea is that this method should be an extension method relevant only within the query. |
If that's your goal, then why does it have to be an extension method? Why not a simple Posts navigation property which can be translated in an EF LINQ query but throws when invoked? |
You mean with getter which always returns null/throws? Is not EF accessing navigation properties on SaveChanges? |
We're discussing a scenario where there is no navigation at all, right? In other words, you could have a Posts property, but tell EF to ignore it (just like it would ignore a Posts() extension method, which is what you're proposing above). So assuming you don't want EF to actually have the navigation, why not just have an unmapped property there instead of an extension method? To be clear, neither is supported right now - but I'm trying to understand why you're looking specifically for an extension method rather than a property. |
Ok. let me explain one more time from the very beginning.
If we assume that requested feature is a no go, then we will be fine with explicit joins:
Then there's one more improvement possible.
First two lines are exactly the same as in the first query. What if we could express them as a queryable set?
then query turns into
Slightly shorter, isn't it?
If any of the features were implemented, we'd have an option for convenient querying without affecting the Command processing side. This is the ultimate goal. |
@voroninp thanks for the detailed explanation. I think I understand what you're saying (though I may not think that the added complexity is actually warranted). But my question is actually much simpler... I understand that you don't want navigation properties on your entity CLR types, so that they wouldn't be usable when doing writing/updating (so that you wouldn't accidentally include two aggregates in the same transaction/SaveChanges); but at the same time, you want to be able to use navigations when querying, more or less in the usual way. So there needs to be something that isn't an actual EF navigation, but which can still be used in queries like a navigation, i.e. to tell EF to join with a related entity in the query, etc. Hopefully this summarizes what you're looking for. My only question is why that thing - which can be used in queries but not outside of queries - must be an extension method over the CLR type rather than a property (not auto-property!) on that CLR type. To be extra clear: even as a property, it would still not be usable outside of queries; in other words, the property would only have a getter (no setter), and that getter would throw. So you would be able to reference that property in queries in the usual way - your queries would basically look exactly like regular EF LINQ queries (no weird extension methods), but at the same time they would throw whenever you try to use them outside of queries. Again - I'm not saying this is possible today; there still needs to be some mechanism with EF to configure the property as representing the navigation, even though it has no setter and its getter should never be called. |
Ah... got you now. This would be fine. |
@voroninp we've discussed this as a team, and decided that while shadow navigations aren't a perfect answer here, it's good enough: ctx.Set<Blog>().Where(b => EF.Property<ICollection<Post>>(b, "Posts").Any(p => ...)); EF.Property may add a bit of verboseness, but this is the pattern already recommended e.g. for keeping database ID properties out of CLR types. We don't plan to do the (significant) extra work to allow defining a property/method that would be usable as a navigation in queries, but not otherwise. @AndriySvyryd I've looked around and in #240, and haven't found an issue tracking shadow navigations - do you remember if we have one? |
This would be ok. But I hope custom transformations of expression trees will be supported in the future anyway. |
@voroninp have you looked at the LINQ expression tree interceptor introduced in EF 7.0? |
@roji Wow, I was not aware of it. Thanks! |
It was this one #3864 |
@roji , just if you are curious how people use EF in the context of DDD ;-) |
More ideas: Usually, entity of type
|
This article also explains the concern https://ardalis.com/navigation-properties-between-aggregates-modules/ |
I need the convenience of navigation properties and want to use them for implicit joins and filtering without having them as CLR properties or fields.
Motivation
Aggregates should not contain referential links to each other.
When aggregate references another one with a key, it may be wise not to put FK constraint. (I know there's an open issue for this)
People abuse navigation properties very often. They add all navigations mimicking the database relations and then return the entity with the subset of includes:
This becomes a nightmare when one must guess somewhere deep in business logic which properties were included and which were not. A business object should not be a dynamic graph which changes its shape based on some implicit context.
I blame ORMs for creating the mindset when people start designing their application from DB:
Arrrghhh!
Let's speculate a bit, now.
Here are two things:
Blog
andPost
. I hope you agree that they usually change independently in different transactions, so they are indeed two distinct aggregates. Hence, post can reference the blog only by id.However, for the read side of my application I may need to build a projection from both of them.
To achieve this, I either must use separate
DbContext
|Types (hello, table splitting with its own problems), or I must join aggregates manually:This quickly becomes ugly, if I need to join more than two aggregates to build a projection. Alternatively, I could switch to
linq
syntax, but explicit join is still required.Now let's assume we are allowed to have shadow navigation properties. The following becomes legit:
Well, hardly better - too verbose.
Let's hope EF 9 finally has the feature to easily define custom expressions for methods or properties, so this becomes possible:
Now I'll be able to call it like this:
Perfect. When filtering, I have the navigations to easily access properties of related aggregates, but when returning the entities, no navigation properties!
The text was updated successfully, but these errors were encountered: