web-dev-qa-db-fra.com

Injection de dépendance dans le modèle d'unité de travail à l'aide de référentiels

Je veux créer une unité de classe de travail qui enveloppe les référentiels de la même manière que this

Le problème que j'ai est d'essayer de mettre en œuvre l'injection de dépendance en remplaçant les référentiels génériques dans l'exemple avec une interface IRepository. Dans l'article lié, ils utilisent des getters pour vérifier si le référentiel est instancié et si ce n'est pas le cas.

public GenericRepository<Department> DepartmentRepository
{
    get
    {
        if (this.departmentRepository == null)
        {
            this.departmentRepository = new GenericRepository<Department>(context);
        }
        return departmentRepository;
    }
}

Ce qui est fortement couplé.

Je peux voir deux façons de contourner cela.

  1. Utilisez l'injection de constructeur.
  2. Utilisez l'injection setter.

Le problème avec 1 est que si j'injecte tous les référentiels, je dois instancier chaque référentiel même si je ne les utilise pas dans cette instance d'unité de travail particulière. Ainsi encourir la surcharge de le faire. J'imaginais utiliser une seule unité de classe de travail à l'échelle de la base de données, ce qui entraînerait beaucoup d'instanciations inutiles et un constructeur gigantesque.

Le problème avec 2 est qu’il serait facile d’oublier de définir et de finir avec des exceptions de référence nulles.

Existe-t-il des meilleures pratiques dans ce scénario? Et y a-t-il d'autres options que j'ai manquées?

Je commence juste à m'injecter de la dépendance et j'ai fait toutes les recherches que je peux trouver sur le sujet mais il se peut que je manque quelque chose de clé.

33
rashleighp

Une façon de procéder consiste à ne pas charger le UnitOfWork de créer chaque Repository par injection de conteneur, mais à le responsabiliser à chaque Repository afin de s’assurer que le UnitOfWork connaisse son existence lors de l’instanciation.

Cela garantira que

  • votre UnitOfWork n'a pas besoin de changer pour chaque nouvelle Repository
  • vous n'utilisez pas un localisateur de service (considéré par beaucoup comme un anti-pattern )

Ceci est mieux démontré avec du code - j'utilise SimpleInjector donc les exemples sont basés sur ceci:

En commençant par l'abstraction Repository:

public interface IRepository 
{
    void Submit();
}
public interface IRepository<T> :IRepository where T : class { }
public abstract class GenericRepository<T> : IRepository<T> where T : class { }

et le UnitOfWork

public interface IUnitOfWork
{
    void Register(IRepository repository);
    void Commit();
}

Chaque Repositorydoit s'enregistrer avec le UnitOfWork et vous pouvez le faire en modifiant la classe abstraite parent GenericRepository pour vous assurer que c'est fait:

public abstract class GenericRepository<T> : IRepository<T> where T : class
{
    public GenericRepository(IUnitOfWork unitOfWork)
    {
        unitOfWork.Register(this);
    }
}

Chaque réel Repository hérite de GenericRepository:

public class Department { }
public class Student { }

public class DepartmentRepository : GenericRepository<Department> 
{
    public DepartmentRepository(IUnitOfWork unitOfWork): base(unitOfWork) { }
}

public class StudentRepository : GenericRepository<Student>
{
    public StudentRepository(IUnitOfWork unitOfWork) : base(unitOfWork) { }
}

Ajoutez l'implémentation physique de UnitOfWork et vous êtes tous ensemble:

public class UnitOfWork : IUnitOfWork
{
    private readonly Dictionary<string, IRepository> _repositories;
    public UnitOfWork()
    {
        _repositories = new Dictionary<string, IRepository>();
    }

    public void Register(IRepository repository)
    {
        _repositories.Add(repository.GetType().Name, repository);
    }

    public void Commit()
    {
        _repositories.ToList().ForEach(x => x.Value.Submit());
    }
}

L'enregistrement de conteneur peut être configuré pour récupérer automatiquement toutes les instances définies de IRepository et les enregistrer avec une étendue de durée de vie afin de s'assurer qu'elles survivent pendant toute la durée de votre transaction:

public static class BootStrapper
{
    public static void Configure(Container container)
    {
        var lifetimeScope = new LifetimeScopeLifestyle();

        container.Register<IUnitOfWork, UnitOfWork>(lifetimeScope);

        container.RegisterManyForOpenGeneric(
            typeof(IRepository<>),
            lifetimeScope,
            typeof(IRepository<>).Assembly);
    }
}

Avec ces abstractions et une architecture construite autour de DI, vous avez un UnitOfWork qui connaît tous les Repository qui ont été instanciés dans tout appel de service et vous avez la validation de la compilation lorsque tous vos référentiels ont été définis. Votre code est ouvert pour extension mais fermé pour modification .

Pour tester tout cela, ajoutez ces classes

public class SomeActivity
{
    public SomeActivity(IRepository<Department> departments) { }
}

public class MainActivity
{
    private readonly IUnitOfWork _unitOfWork;
    public MainActivity(IUnitOfWork unitOfWork, SomeActivity activity) 
    {
        _unitOfWork = unitOfWork;
    }

    public void test()
    {
        _unitOfWork.Commit();
    }
}

Ajoutez ces lignes à BootStrapper.Configure()

//register the test classes
container.Register<SomeActivity>();
container.Register<MainActivity>();

Placez un point d'arrêt contre la ligne de code:

_repositories.ToList().ForEach(x => x.Value.Submit());

Et enfin, lancez ce code de test de la console:

class Program
{
    static void Main(string[] args)
    {
        Container container = new Container();
        BootStrapper.Configure(container);
        container.Verify();
        using (container.BeginLifetimeScope())
        {
            MainActivity entryPoint = container.GetInstance<MainActivity>();
            entryPoint.test();
        }
    }
}

Vous constaterez que le code s'arrête au point d'arrêt et que vous avez une instance active de IRepository prête et en attente de Submit() toute modification apportée à la base de données.

Vous pouvez décorer votre UnitOfWork pour gérer les transactions, etc. Je vais maintenant céder la place au puissant .NetJunkie et vous recommander de lire ces deux articles ici et ici .

46
qujck

Au lieu d'injecter des instances de référentiel, injectez un seul objet de fabrique qui sera responsable de la création de ces instances. Vos accesseurs utiliseront ensuite cette usine.

5
Ladislav Mrnka

Ma solution est UnitOfWork toujours responsable de la création du référentiel, mais j'ai créé une méthode GetRepository () factory dans UnitOfWork.

public interface IUnitOfWork : IDisposable
{
    T GetRepository<T>() where T : class;
    void Save();
}

public class UnitOfWork : IUnitOfWork
{
    private Model1 db;

    public UnitOfWork() :  this(new Model1()) { }

    public UnitOfWork(TSRModel1 dbContext)
    {
        db = dbContext;
    }

    public T GetRepository<T>() where T : class
    {          
        var result = (T)Activator.CreateInstance(typeof(T), db);
        if (result != null)
        {
            return result;
        }
        return null;
    }

    public void Save()
    {
        db.SaveChanges();
    }

    private bool disposed = false;

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                db.Dispose();
            }
        }
        this.disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

public class TestRepository : GenericRepository<Test>, ITestRepository
{
    public TestRepository(Model1 db)
       : base(db)
    {
    }
}

public class TestManager: ITestManager
{
    private IUnitOfWork unitOfWork;
    private ITestRepository testRepository;
    public TestManager(IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
        testRepository = unitOfWork.GetRepository<TestRepository>();
    }

}
0
sonmt