Open dazinator opened 3 years ago
However, I want to make it explicit that this User entity should only be tracked in DbContext A, and not tracked in DbContext B.
I don't think you're going to be able to do this if the entity is part of the graph being tracked and is needed for the correct update to be generated. If it's not needed for the update, then you could not track it, but this would have to be done manually. I wouldn't recommend trying to do this.
I have seen from here: https://stackoverflow.com/questions/9415334/entity-framework-code-first-readonly-entity that you can use AsNoTracking()
If you don't need the entity to be in the graph for the update, then this would work. But typically then you just wouldn't have the entity present at all.
However is there anything I can do at the model level - similar to the exclude migration api I linked above, that tells DbContext B to never track this entity.
No.
I call SaveChanges() on DbContext A which inserts the User entity. I call SaveChanges() on DbContext B, which, sees the navigation property on the Foo entity to the User entity - and also tries to insert a new User entity.
After step 3, the entity tracked by context B is no longer in sync with the state of the entity in the database. You'll need to update its state to reflect that of the database. For example, it may need to have generated key values propagated.
In general, I think you're probably looking for read-only entities--the issue you referenced above. However, implementation of that issue would not result in entities not being tracked, but rather not allowing changes. In your case, you want the changes to be ignored, which is one option for read-only entities, the other being to throw if they are changed. However, it's still not clear to me how you would deal with propagating values correctly.
Thanks.
After step 3, the entity tracked by context B is no longer in sync with the state of the entity in the database. You'll need to update its state to reflect that of the database. For example, it may need to have generated key values propagated.
Not sure I understand that bit! In my scenario, its the same instance of the entity that's being tracked by both context A and context B. So, after calling SaveChanges() on context A, I'm expecting auto generated values that need to be propagated, to be mediated accross the two contexts, by the entity's own property values, which should now be set. I understand if I was using shadow properties that this state would be inaccessible to the other dbcontext but I'm not using shadow properties.
In general, I think you're probably looking for read-only entities--the issue you referenced above
I believe this to be true, except having to do AsNoTracking for each query rather than defining a particular entity type to be read only at a model level seems problematic for the code base and a possible source of error. Perhaps I am missing a trick.
I think.. Im looking for a feature to better support this scenario with bounded contexts concisely. I feel its a requirement really, for full "bounded context" support?
1.Entities A, B, C in bounded context 1 need to "relate" to an entity D in another seperate bounded context 2.
I should stop here and say - would be great to understand how the EF core team thinks this should be modelled with current EF features, in a best practice way, or if its not supported, make that explicit in the docs that talk about bounded contexts as that could be considered a deal breaking limitation for some.
Thinking you get the gist :-)
@dazinator
I recently had a near success trying to achieve the same thing using read-only entities. Using the read-only metadata like this also allowed me to override the save methods, and mark all of the read-only entities as either unchanged or detached, so that the DB context doesn't try to persist an entity that it's not responsible for tracking. It appeared to work great at first.
I eventually ran into problems with just adding an entity. It may have partly been an issue with the infrastructure for my current project, but I couldn't get this worked out.
Take the scenario: entity A in context A, and entity B in context B, so that B has a FK to entity A; I ran into problems with this when I insert B { ID=1, A_FK=1 }
into context B, then insert B { ID=2, A_FK=1 }
. The context B appropriately marked A#1 as read-only/detached/unchanged. But when I tried to insert the second time, which had the same reference to A, I could not get around the error that context B always said there was a duplicate key for A#1. I tried several approaches to making the context understand that it's the same A#1, not a duplicate. But I had not success with that.
I'd be interested to see if that idea gets you anywhere, and if you are able to come up with something to get over the problems I had.
@dazinator There is definitely room to make this experience better. My gut feeling is that using read-only entities is the way to handle this, rather than through attempting not to track in one context or the other. A proper implementation of read-only entity types is tracked by #7586. I'm going to make a note on that issue to consider this scenario, and put this in the backlog to specifically track the bounded context scenario, since read-only entity types are much more general than just this.
I'd suggest being very careful with approach. I can see the desire to incrementally move towards bounded contexts, but it's really just a facade:
Have you looked into using composition (either server-side or client-side) instead? It'd look something like this:
Saving
UserService.Save(user);
foo.UserId = user.UserId; // somewhere
FooService.Save(foo);
Reading
var foos = FooService.Get(fooIds);
var userIds = foos.Select(f => f.UserId);
var users = UserService.Get(userIds);
var composed = // in memory join between foos and users
I think you'll find this pattern will set you up much better for success. Also, if you're not already following him, Udi Dahan has some really good content around finding service boundaries. e.g. https://www.youtube.com/watch?v=RhfyP8pEEc4
@optiks Thanks! I understand the approach you've shown offers much cleaner isolation between the boundaries. It's a bit of a tradeoff because:
We will have to perform two queries when loading data instead of one, and do an in memory join on the client side. This enforces a stricter boundary but at a perf penalty!
Although (thanks to the stricter boundary) it does allows us to move the data for each bounded context to its own seperate data store in future (which gives us greater scalability options etc) - if we are comfortable that we always want this data to remain in the same database "together" and are happy that such a change is highly unlikely - then we are losing the potential benefits around data integrity that having a foreign key within the dame database enforces. It's very nice that when deleting a user we know that their related data will be deleted in the same transaction. In a microservice architecture with strict boundaries the best you often have is eventual consistency.
I agree with you we need to be careful and make an informed decision! Thanks for your input it will be useful to discuss this with my team :-)
@dazinator No worries at all. Just a couple more thoughts:
User
entities -- A.User
and B.User
. This will let you:
A
and B
B
needsB.User
properties read-only (via https://docs.microsoft.com/en-us/ef/core/modeling/constructors). That will at least stop any accidental updates (and you can block any other inserts/updates/deletes by overriding B.SaveChanges(...)
or similar).Good luck!
@optiks An approach we considered that was closer to your #2 suggestion above, was having that second entity mapped to a SQL View instead, where the VIEW does the join to pull the additional data, and obviously doesn't allow inserts / updates or deletes. I think this is slightly safer, and less work at the EF level. However with this approach you lose compile time errors if changes are made to the user table / user entity, the T-SQL veiw can become broken, and your data model with the view entity compiles fine - oblivious to the error until runtime! Just mentioning it as this might work for some.
@optiks Actually I think this might work:
Bounded context that owns the User
Entity also defines a SQL view to provide read only access to User information.
Other bounded contexts that need to save records linked to a user just use @optiks approach above where they set the UserId key (no navigation entity). The SQL database schema has a foreign key constraint but the EF model doesn't need to know this. So saving a record looks like this as @optiks said:
UserService.Save(user);
foo.UserId = user.UserId; // somewhere
FooService.Save(foo);
In the case the other bounded contexts need to load bulk information supplemented with User
information, they can define their own VIEW, that will join to the User view, to pull in the additional columns. They can define their own entity that maps to this view i.e FooWithUserInfo
.
In this scenario:-
Users
view is the contract.. when we make changes in the bounded context that owns the user entity we need to be clear that we can't alter the view with breaking changes. That is, we can add new columns to the Users view, but can't remove or rename existing columns, or change existing columns data types in breaking ways. Otherwise we will break other contexts that rely on this contract.I think this might be a good fit for us.
Database objects would look like:
Schema | Object Type | Name |
---|---|---|
ContextA | Table | User |
ContextA | View | UserInfo |
ContextB | Table | Foo |
ContextB | View | FooWithUserInfo |
Ask a question
I am using bounded contexts. This means I have two different DbContexts, with their own entities, and own "Schema", but sharing the same connection string.
My question comes where there is a relationship between the boundaries. In DbContext A there is a
User
Entity. In DbContext B there is a need to "join" someFoo
entity to theUser
entity - so I want to model this as a navigation property (from Foo --> User).I have found that I can include the
User
entity into the model for DbContext B - and ignore it for migration purposes as documented here: https://docs.microsoft.com/en-us/ef/core/modeling/entity-types?tabs=data-annotations#excluding-from-migrationsHowever, I want to make it explicit that this
User
entity should only be tracked in DbContext A, and not tracked in DbContext B. This is because responsibility for the Create, Update and Delete of theUser
entity should only ever be performed in DbContext A. DbContext B just needs the ability to read and join to this entity in it's model.I have seen from here: https://stackoverflow.com/questions/9415334/entity-framework-code-first-readonly-entity that you can use
AsNoTracking()
on a per query level to do this. However is there anything I can do at the model level - similar to the exclude migration api I linked above, that tells DbContext B to never track this entity.The issue I am trying to avoid is this:
Foo
entity in DbContext B, and sets a navigation property on theFoo
entity, to theUser
entity thats already been added to DbContext A, and was not tracked previously by DbContext B.Foo
entity to theUser
entity - and also tries to insert a new User entity.Note: I haven't actually tested this scenario yet but I am assuming this problem would occur based on my current understanding of EF Core and how it wants to track entities. Between steps 3 and 4, the
User
entity will have been inserted but DbContext B will not know that - despite theUser
entity has now been assigned an Id by DbContext A after the insert.Rather than addressing this on a per-query basis, I'd like to know if there is something I can do at a model level so better isolate entities to prevent them from being tracked in contexts that should not "own" them.
Include provider and version information
EF Core version: 5.0.0 Database provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer): Microsoft.EntityFrameworkCore.SqlServer Target framework: (e.g. .NET 5.0): .NET 5.0 Operating system: Windows 10 IDE: (e.g. Visual Studio 2019 16.3): Visual Studio 2019 Pro 16.8.2