0
\$\begingroup\$

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, was TTransactionType, since each Database driver manufacturer has a different implementation of transaction handling. Like:

    • MongoDB = IClientSessionHandler.cs
    • MySql = MySqlTransaction.cs
    • Oracle = OracleTransaction.cs
  • 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)
    {
        /*...*/
    }
}
  1. "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.

\$\endgroup\$
2
  • \$\begingroup\$ This looks very abstract. Have you used this in an actual project yet? \$\endgroup\$
    – Mast
    Commented 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
    Commented Oct 8, 2020 at 15:53

3 Answers 3

1
\$\begingroup\$

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);
}
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your answer! In the GitHub link solution, I tried to bring the responsibility of managing a transaction to my DbContext Please see it. :) \$\endgroup\$
    – Igor
    Commented Oct 12, 2020 at 3:05
0
\$\begingroup\$

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.

https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application

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.

\$\endgroup\$
4
  • \$\begingroup\$ The Unit Of Work not contains any bussiness logic. The question is specific to Unit 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
    Commented 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\$ Commented 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
    Commented Oct 9, 2020 at 22:51
  • \$\begingroup\$ And please, not down vote the question! \$\endgroup\$
    – Igor
    Commented Oct 9, 2020 at 22:56
0
\$\begingroup\$

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.

\$\endgroup\$
3
  • \$\begingroup\$ Hey @Charles! You read the READ.md of git repo question? \$\endgroup\$
    – Igor
    Commented Oct 12, 2020 at 2:53
  • \$\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 the GetCurrentTransaction method because the one who calls it are the repositories and each database has a different implementation over the commands, that is, each has its own syntax. \$\endgroup\$
    – Igor
    Commented 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\$ Commented Oct 12, 2020 at 2:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.