web-dev-qa-db-fra.com

Comment mettre en œuvre le modèle d'unité de travail avec Dapper?

Actuellement, j'essaie d'utiliser Dapper ORM avec Unit Of Work + Repository Pattern.

Je souhaite utiliser Unit of Work par opposition à un simple référentiel dapper, car mon insertion et mes mises à jour nécessitent un certain degré de traitement des transactions. Je n'ai pas pu trouver d'exemples utiles car la plupart semblent utiliser Entity Framework et ont des problèmes de fuite au sein de l'unité de travail.

Quelqu'un pourrait-il m'orienter dans la bonne direction?

33
Stig

Ce projet Git est très utile. Je suis parti de la même chose et j'ai fait quelques changements selon mes besoins.

public sealed class DalSession : IDisposable
{
    public DalSession()
    {
        _connection = new OleDbConnection(DalCommon.ConnectionString);
        _connection.Open();
        _unitOfWork = new UnitOfWork(_connection);
    }

    IDbConnection _connection = null;
    UnitOfWork _unitOfWork = null;

    public UnitOfWork UnitOfWork
    {
        get { return _unitOfWork; }
    }

    public void Dispose()
    {
        _unitOfWork.Dispose();
        _connection.Dispose();
    }
}

public sealed class UnitOfWork : IUnitOfWork
{
    internal UnitOfWork(IDbConnection connection)
    {
        _id = Guid.NewGuid();
        _connection = connection;
    }

    IDbConnection _connection = null;
    IDbTransaction _transaction = null;
    Guid _id = Guid.Empty;

    IDbConnection IUnitOfWork.Connection
    {
        get { return _connection; }
    }
    IDbTransaction IUnitOfWork.Transaction
    {
        get { return _transaction; }
    }
    Guid IUnitOfWork.Id
    {
        get { return _id; }
    }

    public void Begin()
    {
        _transaction = _connection.BeginTransaction();
    }

    public void Commit()
    {
        _transaction.Commit();
        Dispose();
    }

    public void Rollback()
    {
        _transaction.Rollback();
        Dispose();
    }

    public void Dispose()
    {
        if(_transaction != null)
            _transaction.Dispose();
        _transaction = null;
    }
}

interface IUnitOfWork : IDisposable
{
    Guid Id { get; }
    IDbConnection Connection { get; }
    IDbTransaction Transaction { get; }
    void Begin();
    void Commit();
    void Rollback();
}

Maintenant, vos référentiels devraient accepter cette UnitOfWork d'une manière ou d'une autre. Je choisis l'injection de dépendances avec le constructeur.

public sealed class MyRepository
{
    public MyRepository(IUnitOfWork unitOfWork) 
    {
        this.unitOfWork = unitOfWork;
    }

    IUnitOfWork unitOfWork = null;

    //You also need to handle other parameters like 'sql', 'param' ect. This is out of scope of this answer.
    public MyPoco Get()
    {
        return unitOfWork.Connection.Query(sql, param, unitOfWork.Transaction, .......);
    }

    public void Insert(MyPoco poco)
    {
        return unitOfWork.Connection.Execute(sql, param, unitOfWork.Transaction, .........);
    }
}

Et puis vous l'appelez comme ceci:

Avec transaction:

using(DalSession dalSession = new DalSession())
{
    UnitOfWork unitOfWork = dalSession.UnitOfWork;
    unitOfWork.Begin();
    try
    {
        //Your database code here
        MyRepository myRepository = new MyRepository(unitOfWork);
        myRepository.Insert(myPoco);
        //You may create other repositories in similar way in same scope of UoW.

        unitOfWork.Commit();
    }
    catch
    {
        unitOfWork.Rollback();
        throw;
    }
}

Sans transaction:

using(DalSession dalSession = new DalSession())
{
    //Your database code here
    MyRepository myRepository = new MyRepository(dalSession.UnitOfWork);//UoW have no effect here as Begin() is not called.
    myRepository.Insert(myPoco);
}

Veuillez noter que UnitOfWork est plus que DBTransaction.

Plus de détails sur le référentiel dans le code ci-dessus peuvent être trouvés ici .

J'ai déjà posté ce code ici . Mais cette question me semble plus pertinente pour ce code; donc je poste à nouveau au lieu de simplement un lien vers la réponse originale.

27
Amit Joshi

Edit 2018-08-03: Le commentaire d'Amit m'a vraiment fait réfléchir et m'a fait réaliser que le référentiel n'a pas en fait besoin d'être des propriétés sur le contexte lui-même. Mais plutôt, les référentiels pourraient avoir une dépendance sur le contexte. Plutôt que de continuer à apporter des modifications incrémentielles aux exemples de code ci-dessous. Je vais simplement référencer un git repo que j'ai mis en place pour contenir ce concept.

Debout sur les épaules des autres ici.

Considérant que cette réponse est en tête de liste dans la plupart des recherches Google concernant "dapper" et "unit of work". Je voulais fournir mon approche, que j'ai utilisée à bon escient à plusieurs reprises maintenant.

En utilisant un exemple fictif (et trop simplifié):

public interface IUnitOfWorkFactory
{
    UnitOfWork Create();
}

public interface IDbContext 
{
    IProductRepository Product { get; set; }

    void Commit();
    void Rollback();
}

public interface IUnitOfWork
{
    IDbTransaction Transaction { get;set; }

    void Commit();
    void Rollback();
}


public interface IProductRepository 
{
    Product Read(int id);
}

Notez comment ni IDbContext ni IUnitOfWorkFactory n'implémente IDisposable. Ceci est délibérément fait pour éviter une abstraction qui fuit . Au lieu de cela, la dépendance repose sur Commit()/Rollback() pour prendre soin du nettoyage et de l'élimination.

Quelques points avant de partager les implémentations.

  • IUnitOfWorkFactory est responsable de l'instanciation du UnitOfWork et du courtage de la connexion à la base de données.
  • IDbContext est l'épine dorsale du référentiel.
  • IUnitOfWork est une encapsulation de IDbTransaction, et garantit que lorsqu'ils travaillent avec plusieurs référentiels, ils partagent un même contexte de base de données.

Implémentation de IUnitOfWorkFactory

public class UnitOfWorkFactory<TConnection> : IUnitOfWorkFactory where TConnection : IDbConnection, new()
{
    private string connectionString;

    public UnitOfWorkFactory(string connectionString)
    {
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new ArgumentNullException("connectionString cannot be null");
        }

        this.connectionString = connectionString;
    }

    public UnitOfWork Create()
    {
        return new UnitOfWork(CreateOpenConnection());
    }

    private IDbConnection CreateOpenConnection()
    {
        var conn = new TConnection();
        conn.ConnectionString = connectionString;

        try
        {
            if (conn.State != ConnectionState.Open)
            {
                conn.Open();
            }
        }
        catch (Exception exception)
        {
            throw new Exception("An error occured while connecting to the database. See innerException for details.", exception);
        }

        return conn;
    }
}

Implémentation de IDbContext

public class DbContext : IDbContext
{
    private IUnitOfWorkFactory unitOfWorkFactory;

    private UnitOfWork unitOfWork;

    private IProductRepository product;

    public DbContext(IUnitOfWorkFactory unitOfWorkFactory)
    {
        this.unitOfWorkFactory = unitOfWorkFactory;
    }

    public ProductRepository Product =>
        product ?? (product = new ProductRepository(UnitOfWork));

    protected UnitOfWork UnitOfWork =>
        unitOfWork ?? (unitOfWork = unitOfWorkFactory.Create());

    public void Commit()
    {
        try
        {
            UnitOfWork.Commit();
        }
        finally
        {
            Reset();
        }
    }

    public void Rollback()
    {
        try
        {
            UnitOfWork.Rollback();
        }
        finally
        {
            Reset();
        }
    }

    private void Reset()
    {
        unitOfWork = null;
        product = null;
    }
}

Implémentation de IUnitOfWork

public class UnitOfWork : IUnitOfWork
{
    private IDbTransaction transaction;

    public UnitOfWork(IDbConnection connection)
    {
        transaction = connection.BeginTransaction();
    }

    public IDbTransaction Transaction =>
        transaction;

    public void Commit()
    {
        try
        {
            transaction.Commit();
            transaction.Connection?.Close();
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
        finally
        {
            transaction?.Dispose();
            transaction.Connection?.Dispose();
            transaction = null;
        }
    }

    public void Rollback()
    {
        try
        {
            transaction.Rollback();
            transaction.Connection?.Close();
        }
        catch
        {
            throw;
        }
        finally
        {
            transaction?.Dispose();
            transaction.Connection?.Dispose();
            transaction = null;
        }
    }
}

Implémentation de IProductRepository

public class ProductRepository : IProductRepository
{
    protected readonly IDbConnection connection;
    protected readonly IDbTransaction transaction;

    public ProductRepository(UnitOfWork unitOfWork)
    {
      connection = unitOfWork.Transaction.Connection;
      transaction = unitOfWork.Transaction;
    }

    public Product Read(int id)
    {
        return connection.QuerySingleOrDefault<Product>("select * from dbo.Product where Id = @id", new { id }, transaction: Transaction);
    }
}

Pour accéder à la base de données, il suffit d'instancier DbContext ou d'injecter en utilisant le conteneur IoC de votre choix (j'utilise personnellement le conteneur IoC fourni par . NET Core ).

var unitOfWorkFactory = new UnitOfWorkFactory<SqlConnection>("your connection string");
var db = new DbContext(unitOfWorkFactory);

Product product = null;

try 
{
    product = db.Product.Read(1);
    db.Commit();
}
catch (SqlException ex)
{
    //log exception
    db.Rollback();
}

Le besoin explicite de Commit() pour cette simple opération en lecture seule semble excessif, mais porte ses fruits à mesure que le système se développe. Et apparemment, offre un avantage mineur en termes de performances selon Sam Saffron . Vous "pouvez" également omettre la db.Commit() sur les opérations de lecture simples, en faisant cela en laissant la connexion ouverte et en mettant le fardeau du nettoyage sur le ramasse-miettes. Ce n'est donc pas recommandé.

J'apporte généralement le DbContext dans le giron au niveau du service, où il fonctionne à l'unisson avec d'autres services pour former le "ServiceContext". Je fais ensuite référence à ce ServiceContext dans la couche MVC réelle. Comme autre point de mention, il est recommandé d'utiliser async dans toute la pile si vous le pouvez. Il est omis ici pour des raisons de simplicité.

13
pim