dotnet / EntityFramework.Docs

Documentation for Entity Framework Core and Entity Framework 6
https://docs.microsoft.com/ef/
Creative Commons Attribution 4.0 International
1.62k stars 1.96k forks source link

Multiple db contexts in retryable transaction #4807

Open dazinator opened 2 weeks ago

dazinator commented 2 weeks ago

Type of issue

Missing information

Description

Suppose my database has different schema's and I have seperate DbContext's for those schemas.

Given the retry policy / execution strategy is configured per DB context, how should I Execute a "resilient transaction" that spans multiple Db Contexts?

Also given people have standardised on using polly for resiliency - can it be mentioned why polly isn't used here? Or is there a way to use polly for this use case instead that could be linked to for those that adopt Polly for resiliency already? Polly is such a big part of the resiliency story in dotnet that I feel it should atleast be mentioned.

Also, should there be some guidance around not putting "side effects" in the delegate that is "retried". I think anything in this method should be idempotent right?

Page URL

https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency

Content source URL

https://github.com/dotnet/EntityFramework.Docs/blob/main/entity-framework/core/miscellaneous/connection-resiliency.md

Document Version Independent Id

29002343-fa42-2f49-3362-53592039b26d

Article author

@AndriySvyryd

AndriySvyryd commented 1 week ago

Execute a "resilient transaction" that spans multiple Db Contexts?

Unless they share the same connection object it would need to be a distributed transaction. It's possible to implement via a TransactionScope, but few providers support it and it's generally not recommended.

can it be mentioned why polly isn't used here?

roji commented 1 week ago

Note https://github.com/dotnet/efcore/issues/30552 which tracks EF and Polly integration. I do believe it may make sense to do something here, possibly as an additional package (Microsoft.EntityFrameworkCore.Polly?).

dazinator commented 1 week ago

but few providers support it and it's generally not recommended.

Most ado.net providers do support TransactionScope for local transactions as far as I am aware. For example SqlServer, MySql, Postgres to name just the first few I checked. If there is a list somewhere showing which ones don't that would be useful to know!

EfCore dbcontext seems to work fine with TransactionScope, atleast until you add an IExecutionPolicy.. which is where "suprise" kicks in. After careful consideration I think a better design is to not use EF core retry policy. I've settled on a design where you have your own IUnitOfWork abstraction and where potentially multiple DbContexts (repositories encapsulating dbcontexts) or other subsystems (Dapper repositories etx) can all participate in the same local transaction - and the resilience is handled at that unit of work level - using polly. This ensures you can mix technologies (dapper, efcore, mass transit outbox message publishing etc) in a local transaction and have a sane place to configure the resilience aspect that isn't tied to just one of them, but more tied to the datastore in use by your application (I.e sql server)

Let's treat the guidance on Distributed transactions as seperate because I agree these are generally not recommended..However TransactionScope only attempts to upgrade to a distributed transaction when you attempt to use transaction spanning multiple different data stores. I'd argue that this isn't the only use case for TransactionScope and that it's actually very beneficial to use it for local transaction against a single data source. Consider the following common pattern - the transactional outbox pattern. In this pattern you often have to update some records using EFCore as one subsystem, and say, MassTransit participating in the same transaction as another subsystem - as it wants to write to an outbox table. Because both subsystems support Transaction Scope, its very easy to wrap them in a transaction scope, and because both subsystems are writing to the same data store, there is no distributed transaction that comes into play, and the code is also more solid because we didn't have to modify any of our repository code to manually hook up the mass transit transaction with the dbcontext database transaction to accomodate the transactional participant, its plug and play.

So my thoughts are: the narrative that most providers don't support Transaction Scope and that in general they are not recommended could be refined to something more nuanced: Most popular sql providers do support TransactionScope for local transactions / i.e transactions spanning a single connection string or data store; but its not recommended to use them in situations where you will be spanning potentially multiple data stores as this gives rise to distributed transactions. In general distributed transactions are not recommended due to: [reasons - scalability etc]. This way it doesn't alienate those that want to use TransactionScope for purely local transactions as being bad engineers for going against recommendations. Unless there are other reasons it's frowned upon that I am unaware of perhaps?

roji commented 1 week ago

@dazinator I'd recommend not conflating things. EF's retrying strategy can be used with multiple contexts without TransactionScope; you need to ensure to use the same connection and transaction by all contexts, but this is the case regardless of whether you use TransactionScope or not (otherwise you get a distributed transaction).

Of course, if you want to use Polly rather than EF's retrying strategy, that's totally fine - but as far as I'm aware there's nothing related to "multiple db context" which favors one over the other. Similarly, if you want to use TransactionScope you may do that, but that's again orthogonal.

Re TransactionScope specifically, It's true that it's supported by most provider (IIRC Microsoft.Data.Sqlite is the notable exception). However, the API doesn't support asynchronous I/O (for commit/rollback), for example.