Skip to content
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

Allow entity instances to be tracked by two bounded contexts, but limit updates to one context #23457

Open
Tracked by #22954
dazinator opened this issue Nov 24, 2020 · 9 comments

Comments

@dazinator
Copy link

dazinator commented Nov 24, 2020

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" some Foo entity to the User 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-migrations

However, 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 the User 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:

  1. In my application I use some service that creates and adds a User entity to DbContext A.
  2. I then use another service that creates a Foo entity in DbContext B, and sets a navigation property on the Foo entity, to the User entity thats already been added to DbContext A, and was not tracked previously by DbContext B.
  3. I call SaveChanges() on DbContext A which inserts the User entity.
  4. 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.

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 the User 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

@ajcvickers
Copy link
Member

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.

@dazinator
Copy link
Author

dazinator commented Dec 1, 2020

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.

  1. You want to model that as a navigation property because its a foreign key relationship.
  2. You want entity D to be readonly within bounded context 1 because its managed by bounded context 2.
  3. You want that nav property to be seen as special. It should be used to set the foreign key value for the foreign key relationship, when inserting or updating entities A, B, C nothing else.
  4. You want to query entities A, B, C and use the nav property in those queries. Any instances of entity D materialised through a query on that nav property though, should always be read only. Any updates to entity D must be done through db context A.
  5. Entity D should be excluded from migrations for dbcontext 1 (this is now possible in 5.0.0) but there are still foreign key relationships to it, that need to be included in migrations.

Thinking you get the gist :-)

@LavenHook
Copy link

@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.

@ajcvickers
Copy link
Member

@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.

@ajcvickers ajcvickers changed the title Bounded contexts - make one dbcontext responsible for tracking Allow entity instances to be tracked by two bounded contexts, but limit updates to one context Dec 9, 2020
@ajcvickers ajcvickers added this to the Backlog milestone Dec 11, 2020
@optiks
Copy link

optiks commented Feb 27, 2021

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:

  • You're relying on the two entities being in the same database (to support the join)
  • You're relying on the same entity (and thus contract) being shared across contexts https://martinfowler.com/bliki/BoundedContext.html
  • You haven't mentioned it, but I'm assuming there's a transactional boundary around all of this

Have you looked into using composition (either server-side or client-side) instead? It'd look something like this:

Saving

  1. UserService.Save(user);
  2. foo.UserId = user.UserId; // somewhere
  3. FooService.Save(foo);

Reading

  1. var foos = FooService.Get(fooIds);
  2. var userIds = foos.Select(f => f.UserId);
  3. var users = UserService.Get(userIds);
  4. 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

@dazinator
Copy link
Author

@optiks Thanks! I understand the approach you've shown offers much cleaner isolation between the boundaries. It's a bit of a tradeoff because:

  1. 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!

  2. 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 :-)

@optiks
Copy link

optiks commented Feb 27, 2021

@dazinator No worries at all. Just a couple more thoughts:

  1. Obviously it depends on the specific use case, but I've found in practice it's rare that two queries instead of one really matters. Make sure you're not over-optimising! :-)
  2. If you stick with the current approach, you might want to consider creating two User entities -- A.User and B.User. This will let you:
    1. Remove the shared dependency between DbContexts A and B
    2. Allow you to expose only the properties that DbContext B needs
    3. Allow you to make all of the B.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!

@dazinator
Copy link
Author

@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.

@dazinator
Copy link
Author

dazinator commented Feb 27, 2021

@optiks Actually I think this might work:

  1. Bounded context that owns the User Entity also defines a SQL view to provide read only access to User information.

  2. 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);
    
  3. 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:-

  1. The 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants