I need to design a generic "Unit Of Work", basically so that I can "change the context of the Database" only changing the implementation of IUnitOfWork
on my DI container. I will exemplify below.
So far, I have following solution:
IUnitOfWork.cs
public interface IUnitOfWork
{
TTransactionType GetCurrentTransaction<TTransactionType>();
Task CommitAsync(CancellationToken cancellationToken);
Task BeginTransactionAsync(CancellationToken cancellationToken);
}
UnitOfWorkMongoDB.cs
public class UnitOfWorkMongoDB : IUnitOfWork
{
IClientSessionHandle _session;
readonly IMongoDbContext _mongoDbContext;
public UOLMongoDB(IMongoDbContext mongoDbContext)
{
_mongoDbContext = mongoDbContext;
}
public TTransactionType GetCurrentTransaction<TTransactionType>() => (TTransactionType)_session;
public async Task CommitAsync(CancellationToken cancellationToken)
{
await _session.CommitTransactionAsync();
}
public async Task BeginTransactionAsync(CancellationToken cancellationToken)
{
_session = await _mongoDbContext.MongoClient.StartSessionAsync(cancellationToken: cancellationToken);
_session.StartTransaction();
}
}
DI Resolution
services.AddScoped<IUnitOfWork, UnitOfWorkMongoDB>();
Using the solution
public class MyServiceA
{
public MyServiceA(IUnitOfWork unitOfWork)
{
/*...*/
}
}
public class MyServiceB
{
public MyService(IUnitOfWork unitOfWork)
{
/*...*/
}
}
Considerations
The
IUnitOfWork
interface is not really generic, the only "generic thing" used, wasTTransactionType
, since each Database driver manufacturer has a different implementation of transaction handling. Like:- MongoDB =
IClientSessionHandler.cs
- MySql =
MySqlTransaction.cs
- Oracle =
OracleTransaction.cs
- MongoDB =
The
IUnitOfWork
interface is non generic in attempt to avoid refactoring code when we change his implementation on D.I resolution.
Samples with non generic IUnitOfWork
:
services.AddScoped<IUnitOfWork, UnitOfWorkPostgreSQL>(); //OR
services.AddScoped<IUnitOfWork, UnitOfWorkOracle>(); //OR
services.AddScoped<IUnitOfWork, UnitOfWorkMySQL>(); //OR
Dependencies not break:
public class MyServiceA
{
public MyServiceA(IUnitOfWork unitOfWork)
{
/*...*/
}
}
- "Problems" with with a generic
IUnitOfWork<TUnitOfWork>
.
If we change the resolution on D.I, all services dependecies will break, like:
services.AddScoped<IUnitOfWork<UnitOfWorkPostgreSQL>, UnitOfWorkPostgreSQL>();
On MyServiceB.cs
for example, before he depends of IUnitOfWork<UnitOfWorkMongoDB>
and now, my D.I container not solves it. This can cause several changes on application code base.
public class MyServiceB
{
public MyServiceB(IUnitOfWork<UnitOfWorkMongoDB> unitOfWork)
{
/*...*/
}
}
I still feel uncomfortable with the explicit conversion (TTransactionType)_session;
on GetCurrentTransaction<TTransactionType>()
method, is there any other approach to make this method? Or am I worrying too much?
What do you think can be improved in this solution? Any evaluation/improvement is welcome!
P.S I created a little repo with this implementation here.
-
\$\begingroup\$ This looks very abstract. Have you used this in an actual project yet? \$\endgroup\$Mast– Mast ♦2020年10月08日 08:54:09 +00:00Commented Oct 8, 2020 at 8:54
-
\$\begingroup\$ @Mast I'm beggining an project with this now. When you say "This looks very abstract" it's a good or bad point?(rofl) Thanks for your comment! :) \$\endgroup\$Igor– Igor2020年10月08日 15:53:08 +00:00Commented Oct 8, 2020 at 15:53
3 Answers 3
Trying to not promote low-level transaction implementation in initial design, IDbContext is the place for db-transactions.
public interface IDbContext
{
void Commit();
void Rollback();
}
Using "Generics" to implement "Unit Of Work"
public interface IUnitOfWork<T> where T: IDbContext
{
T DbContext { get; }
}
Now the async version of IUnitOfWork
public interface IAsyncUnitOfWork: IUnitOfWork<IDbContext>
{
Task CommitAsync(CancellationToken cancellationToken);
Task BeginTransactionAsync(CancellationToken cancellationToken);
}
Trying to implement abstract system with our setup.
public abstract class AbstractUnitOfWork: IAsyncUnitOfWork
{
public abstract IDbContext DbContext {get; set; }
public AbstractUnitOfWork(IDbContext context)
{
DbContext = context;
}
public abstract Task BeginTransactionAsync(CancellationToken cancellationToken);
public abstract Task CommitAsync(CancellationToken cancellationToken);
}
I have some doubts about the way you choose to implement "unit of work". By definition, this pattern is an abstraction between the database access layer and business logic. Which means that the "unit of work" should not have database code. Having database-specific implementation of the "unit of work" is an indication of wrong approach.
The repository and unit of work patterns are intended to create an abstraction layer between the data access layer and the business logic layer of an application. Implementing these patterns can help insulate your application from changes in the data store ...
Means that you change the data store and keep the business logic and "unit of work" intact.
-
\$\begingroup\$ The
Unit Of Work
not contains any bussiness logic. The question is specific toUnit Of Work
, and this implementation not contais any business logic. The repositories they are treated at another level of abstraction. Implementations only solves a "driver transactions" questions, like:BeginTransaction
,Commit
, etc. Please, see the link that I had put on question: UoW: "Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems." - martinfowler.com/eaaCatalog/unitOfWork.html \$\endgroup\$Igor– Igor2020年10月09日 02:45:36 +00:00Commented Oct 9, 2020 at 2:45 -
\$\begingroup\$ > "The Unit Of Work not contains any bussiness logic." Exactly. But it does not contain any DB-specific code either. It is like a staging section in git - these files are selected for commit but not yet committed. > "Maintains a list of objects affected by a business transaction" so where is this list of objects in your implementation? \$\endgroup\$Alex Netkachov– Alex Netkachov2020年10月09日 09:46:54 +00:00Commented Oct 9, 2020 at 9:46
-
\$\begingroup\$ "so where is this list of objects in your implementation?" The "Transaction Scope" has this objects. Created a repository and now put it on question. Please, see. \$\endgroup\$Igor– Igor2020年10月09日 22:51:16 +00:00Commented Oct 9, 2020 at 22:51
-
\$\begingroup\$ And please, not down vote the question! \$\endgroup\$Igor– Igor2020年10月09日 22:56:27 +00:00Commented Oct 9, 2020 at 22:56
This is pretty abstract and might be a better fit over in Software Engineering or Stack Overflow.
I'm going to leave out if this is a good idea. I don't use repos hardly anymore but only you know if that's required. I think for simple projects they are fine but there are better patterns for more complex projects, IMO.
I don't like the generic as there is no guard on what I can cast it to. What I can't tell from your code is how having this generic wouldn't also prevent code changes needing to happen. For example if injecting IUnitOfWork then in middle of save I have a call to GetCurrentTransaction and now I'm switching to SQL that line of code needs to change and what's worst right now is the compiler wouldn't tell you that and would just get an exception at runtime. Yuck.
I think you need to think about what you all need from each implementation of their transaction and create a common interface and adapt each one to that interface and not directly interact with the providers interface directly in your unit of work. If you need RollBack then add that to the interface and adapt each one to that interface.
You can look at lots of ORMs they do this exact same thing where they work with multiple db engines but have a common way to deal with saving/updating and transactions.
-
-
\$\begingroup\$ >
For example if injecting IUnitOfWork then in middle of save I have a call to GetCurrentTransaction and now I'm switching to SQL that line of code needs to change
< Your affirmation is wrong. If we switched the implementation from IUnitOfWork to a new database type we would not need to change theGetCurrentTransaction
method because the one who calls it are therepositories
and each database has a different implementation over the commands, that is, each has its own syntax. \$\endgroup\$Igor– Igor2020年10月12日 02:53:52 +00:00Commented Oct 12, 2020 at 2:53 -
\$\begingroup\$ First no I didn’t. A good question on code review should not have to follow links to get understanding. Second even so that doesn’t invalidate that there is no constraints and looking at what other ORNs do. \$\endgroup\$CharlesNRice– CharlesNRice2020年10月12日 02:57:15 +00:00Commented Oct 12, 2020 at 2:57