Question Comment supprimer la fonctionnalité d'unité de travail des référentiels à l'aide d'IOC


J'ai une application utilisant ASP.NET MVC, Unity et Linq to SQL.

Le conteneur d'unité enregistre le type AcmeDataContext qui hérite de System.Data.Linq.DataContext, avec un LifetimeManager en utilisant HttpContext.

Il existe une fabrique de contrôleurs qui obtient les instances de contrôleur à l'aide du conteneur d'unité. Je mets en place toutes mes dépendances sur les constructeurs, comme ceci:

// Initialize a new instance of the EmployeeController class
public EmployeeController(IEmployeeService service)

// Initializes a new instance of the EmployeeService class
public EmployeeService(IEmployeeRepository repository) : IEmployeeService

// Initialize a new instance of the EmployeeRepository class
public EmployeeRepository(AcmeDataContext dataContext) : IEmployeeRepository

Chaque fois qu'un constructeur est nécessaire, le conteneur unit résout une connexion, qui est utilisée pour résoudre un contexte de données, puis un référentiel, puis un service et enfin le contrôleur.

Le problème est que IEmployeeRepository expose le SubmitChanges méthode, puisque les classes de service n’ont PAS de DataContext référence.

On m'a dit que l'unité de travail devrait être gérée de l'extérieur des référentiels, il semblerait donc que je devrais supprimer SubmitChanges de mes référentiels. Pourquoi donc?

Si cela est vrai, cela signifie-t-il que je dois déclarer un IUnitOfWork interface et faire chaque la classe de service qui en dépend? Comment puis-je autoriser mes classes de service à gérer l'unité de travail?


10
2017-11-08 22:16


origine


Réponses:


Vous ne devriez pas essayer de fournir le AcmeDataContext lui-même à la EmployeeRepository. Je voudrais même renverser la situation:

  1. Définissez une fabrique permettant de créer une nouvelle unité de travail pour le domaine Acme:
  2. Créer un résumé AcmeUnitOfWork qui résume LINQ to SQL.
  3. Créer une usine concrète qui permet de créer de nouvelles unités de travail LINQ to SQL.
  4. Enregistrez cette usine de béton dans votre configuration DI.
  5. Mettre en place un InMemoryAcmeUnitOfWork pour les tests unitaires.
  6. Vous pouvez éventuellement implémenter des méthodes d'extension pratiques pour les opérations courantes sur votre ordinateur. IQueryable<T> référentiels.

MISE À JOUR: J'ai écrit un article sur ce sujet: Fake ton fournisseur LINQ.

Vous trouverez ci-dessous un exemple étape par étape:

AVERTISSEMENT: Ce sera un message long.

Étape 1: Définition de l'usine:

public interface IAcmeUnitOfWorkFactory
{
    AcmeUnitOfWork CreateNew();
}

La création d’une usine est importante car la DataContext implémenter IDisposable afin que vous vouliez avoir la propriété de l'instance. Alors que certains frameworks vous permettent de disposer des objets quand vous n'en avez plus besoin, les usines le rendent très explicite.

Étape 2: Création d'une unité de travail abstraite pour le domaine Acme:

public abstract class AcmeUnitOfWork : IDisposable
{
    public IQueryable<Employee> Employees
    {
        [DebuggerStepThrough]
        get { return this.GetRepository<Employee>(); }
    }

    public IQueryable<Order> Orders
    {
        [DebuggerStepThrough]
        get { return this.GetRepository<Order>(); }
    }

    public abstract void Insert(object entity);

    public abstract void Delete(object entity);

    public abstract void SubmitChanges();

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

    protected abstract IQueryable<T> GetRepository<T>()
        where T : class;

    protected virtual void Dispose(bool disposing) { }
}

Il y a des choses intéressantes à noter à propos de ce cours abstrait. L'unité de travail contrôle et crée les référentiels. Un dépôt est fondamentalement quelque chose qui implémente IQueryable<T>. Le référentiel implémente des propriétés qui renvoient un référentiel spécifique. Cela empêche les utilisateurs d'appeler uow.GetRepository<Employee>() et cela crée un modèle très proche de ce que vous faites déjà avec LINQ to SQL ou Entity Framework.

L'unité de travail Insert et Delete opérations. Dans LINQ to SQL, ces opérations sont placées sur le Table<T> classes, mais quand vous essayez de le mettre en œuvre de cette manière, cela vous empêchera d'extraire LINQ to SQL.

Étape 3. Créez une usine de béton:

public class LinqToSqlAcmeUnitOfWorkFactory : IAcmeUnitOfWorkFactory
{
    private static readonly MappingSource Mapping = 
        new AttributeMappingSource();

    public string AcmeConnectionString { get; set; }

    public AcmeUnitOfWork CreateNew()
    {
        var context = new DataContext(this.AcmeConnectionString, Mapping);
        return new LinqToSqlAcmeUnitOfWork(context);
    }
}

L’usine a créé un LinqToSqlAcmeUnitOfWorkbasé sur AcmeUnitOfWork classe de base:

internal sealed class LinqToSqlAcmeUnitOfWork : AcmeUnitOfWork
{
    private readonly DataContext db;

    public LinqToSqlAcmeUnitOfWork(DataContext db) { this.db = db; }

    public override void Insert(object entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");
        this.db.GetTable(entity.GetType()).InsertOnSubmit(entity);
    }

    public override void Delete(object entity)
    {
        if (entity == null) throw new ArgumentNullException("entity");
        this.db.GetTable(entity.GetType()).DeleteOnSubmit(entity);
    }

    public override void SubmitChanges();
    {
        this.db.SubmitChanges();
    }

    protected override IQueryable<TEntity> GetRepository<TEntity>() 
        where TEntity : class
    {
        return this.db.GetTable<TEntity>();
    }

    protected override void Dispose(bool disposing) { this.db.Dispose(); }
}

Étape 4: Enregistrez cette usine de béton dans votre configuration DI.

Vous savez vous-même comment bien enregistrer le IAcmeUnitOfWorkFactory interface pour retourner une instance du LinqToSqlAcmeUnitOfWorkFactory, mais ça ressemblerait à ceci:

container.RegisterSingle<IAcmeUnitOfWorkFactory>(
    new LinqToSqlAcmeUnitOfWorkFactory()
    {
        AcmeConnectionString =
            AppSettings.ConnectionStrings["ACME"].ConnectionString
    });

Maintenant, vous pouvez changer les dépendances sur le EmployeeService utiliser le IAcmeUnitOfWorkFactory:

public class EmployeeService : IEmployeeService
{
    public EmployeeService(IAcmeUnitOfWorkFactory contextFactory) { ... }

    public Employee[] GetAll()
    {
        using (var context = this.contextFactory.CreateNew())
        {
            // This just works like a real L2S DataObject.
            return context.Employees.ToArray();
        }
    }
}

Notez que vous pouvez même supprimer le IEmployeeService interface et laisser le contrôleur utiliser le EmployeeService directement. Vous n’avez pas besoin de cette interface pour les tests unitaires, car vous pouvez remplacer l’unité de travail pendant les tests EmployeeService d'accéder à la base de données. Cela vous évitera probablement aussi beaucoup de configuration DI, car la plupart des frameworks DI savent instancier une classe concrète.

Étape 5: Implémenter un InMemoryAcmeUnitOfWork pour les tests unitaires.

Toutes ces abstractions sont là pour une raison. Tests unitaires Maintenant, créons un AcmeUnitOfWork à des fins de test unitaire:

public class InMemoryAcmeUnitOfWork: AcmeUnitOfWork, IAcmeUnitOfWorkFactory 
{
    private readonly List<object> committed = new List<object>();
    private readonly List<object> uncommittedInserts = new List<object>();
    private readonly List<object> uncommittedDeletes = new List<object>();

    // This is a dirty trick. This UoW is also it's own factory.
    // This makes writing unit tests easier.
    AcmeUnitOfWork IAcmeUnitOfWorkFactory.CreateNew() { return this; }

    // Get a list with all committed objects of the requested type.
    public IEnumerable<TEntity> Committed<TEntity>() where TEntity : class
    {
        return this.committed.OfType<TEntity>();
    }

    protected override IQueryable<TEntity> GetRepository<TEntity>()
    {
        // Only return committed objects. Same behavior as L2S and EF.
        return this.committed.OfType<TEntity>().AsQueryable();
    }

    // Directly add an object to the 'database'. Useful during test setup.
    public void AddCommitted(object entity)
    {
        this.committed.Add(entity);
    }

    public override void Insert(object entity)
    {
        this.uncommittedInserts.Add(entity);
    }

    public override void Delete(object entity)
    {
        if (!this.committed.Contains(entity))
            Assert.Fail("Entity does not exist.");

        this.uncommittedDeletes.Add(entity);
    }

    public override void SubmitChanges()
    {
        this.committed.AddRange(this.uncommittedInserts);
        this.uncommittedInserts.Clear();
        this.committed.RemoveAll(
            e => this.uncommittedDeletes.Contains(e));
        this.uncommittedDeletes.Clear();
    }

    protected override void Dispose(bool disposing)
    { 
    }
}

Vous pouvez utiliser cette classe dans vos tests unitaires. Par exemple:

[TestMethod]
public void ControllerTest1()
{
    // Arrange
    var context = new InMemoryAcmeUnitOfWork();
    var controller = new CreateValidController(context);

    context.AddCommitted(new Employee()
    {
        Id = 6, 
        Name = ".NET Junkie"
    });

    // Act
    controller.DoSomething();

    // Assert
    Assert.IsTrue(ExpectSomething);
}

private static EmployeeController CreateValidController(
    IAcmeUnitOfWorkFactory factory)
{
    return new EmployeeController(return new EmployeeService(factory));
}

Étape 6: implémentez éventuellement des méthodes d'extension pratiques:

Les référentiels doivent disposer de méthodes pratiques telles que GetById ou GetByLastName. Bien sûr IQueryable<T> est une interface générique et ne contient pas de telles méthodes. Nous pourrions encombrer notre code avec des appels comme context.Employees.Single(e => e.Id == employeeId), mais c'est vraiment moche. La solution parfaite à ce problème est la suivante: méthodes d’extension:

// Place this class in the same namespace as your LINQ to SQL entities.
public static class AcmeRepositoryExtensions
{
    public static Employee GetById(this IQueryable<Employee> repository,int id)
    {
        return Single(repository.Where(entity => entity.Id == id), id);
    }

    public static Order GetById(this IQueryable<Order> repository, int id)
    {
        return Single(repository.Where(entity => entity.Id == id), id);
    }

    // This method allows reporting more descriptive error messages.
    [DebuggerStepThrough]
    private static TEntity Single<TEntity, TKey>(IQueryable<TEntity> query, 
        TKey key) where TEntity : class
    {
        try
        {
            return query.Single();
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("There was an error " +
                "getting a single element of type " + typeof(TEntity)
                .FullName + " with key '" + key + "'. " + ex.Message, ex);
        }
    }
}

Avec ces méthodes d’extension en place, il vous permet d’appeler ces GetById et d'autres méthodes de votre code:

var employee = context.Employees.GetById(employeeId);

Ce qui est le plus intéressant à propos de ce code (je l'utilise en production), c'est qu'il vous évite d'écrire beaucoup de code pour les tests unitaires. Vous vous retrouverez à ajouter des méthodes à la AcmeRepositoryExtensions classe et propriétés à la AcmeUnitOfWork classe lorsque de nouvelles entités sont ajoutées au système, mais vous n'avez pas besoin de créer de nouvelles classes de référentiel pour la production ou les tests.

Ce modèle a bien sûr quelques résultats. Le plus important est peut-être que LINQ to SQL n'est pas complètement abstrait, car vous utilisez toujours les entités générées par LINQ to SQL. Ces entités contiennent EntitySet<T> propriétés spécifiques à LINQ to SQL. Je ne les ai pas trouvés sur la voie des tests unitaires appropriés, donc pour moi, ce n'est pas un problème. Si vous le souhaitez, vous pouvez toujours utiliser les objets POCO avec LINQ to SQL.

Un autre raccourci est que les requêtes LINQ complexes peuvent réussir en test mais échouent en production, à cause des limitations (ou bogues) du fournisseur de requêtes (en particulier le fournisseur de requêtes EF 3.5 est nul). Lorsque vous n'utilisez pas ce modèle, vous écrivez probablement des classes de référentiel personnalisées qui sont complètement remplacées par des versions de test unitaire et vous ne pourrez toujours pas tester les requêtes dans votre base de données dans les tests unitaires. Pour cela, vous aurez besoin de tests d'intégration, enveloppés par une transaction.

Un dernier raccourci de cette conception est l'utilisation de Insert et Delete méthodes sur l'unité de travail. Tout en les déplaçant dans le référentiel vous obligerait à avoir une conception avec un class IRepository<T> : IQueryable<T> interface, il vous empêche d'autres erreurs. Dans la solution je m'utilise moi aussi j'ai InsertAll(IEnumerable) et DeleteAll(IEnumerable)méthodes. Il est cependant facile de le taper et d’écrire quelque chose comme context.Delete(context.Messages) (noter l'utilisation de Delete au lieu de DeleteAll). Cela compilerait bien, car Delete accepte un object. Une conception avec des opérations de suppression sur le référentiel empêcherait une telle déclaration de compiler, car les référentiels sont saisis.

MISE À JOUR: J'ai écrit un article sur ce sujet qui décrit cette solution plus en détail: Fake ton fournisseur LINQ.

J'espère que ça aide.


24
2017-11-09 05:38



Si vous combinez les unités de travail et les modèles de référentiel, certaines personnes préconisent que l'UoW soit gérée en dehors du référentiel afin de pouvoir créer deux référentiels (CustomerRepository et OrderRepository) et leur transmettre la même instance UOW. être fait atomiquement quand vous appelez enfin UoW.Complete ().

Dans une solution DDD parvenue à maturité, il ne devrait toutefois pas être nécessaire d'utiliser à la fois l'UoW et un référentiel. C'est parce que si une telle solution est définie, les limites des agrégats sont telles qu'il n'y a pas besoin de changements atomiques impliquant plus d'un référentiel.

Est-ce que cela répond à votre question?


2