Как удалить функциональность единицы работы из репозиториев с помощью IOC - PullRequest
10 голосов
/ 09 ноября 2010

У меня есть приложение, использующее ASP.NET MVC, Unity и Linq to SQL.

Контейнер Unity регистрирует тип AcmeDataContext, который наследуется от System.Data.Linq.DataContext, а LifetimeManager - * 1006.*.

Существует фабрика контроллеров, которая получает экземпляры контроллеров, используя контейнер единиц.Я установил все свои зависимости от конструкторов, например:

// 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

Когда нужен конструктор, контейнер Unity разрешает соединение, которое используется для разрешения контекста данных, затем в хранилище, затемслужба и, наконец, контроллер.

Проблема в том, что IEmployeeRepository предоставляет метод SubmitChanges, поскольку классы обслуживания НЕ имеют ссылку DataContext.

Мне сказали, что единица работы должна управляться извне репозиториев, поэтому, похоже, мне следует удалить SubmitChanges из моих репозиториев.Почему это так?

Если это так, значит ли это, что я должен объявить IUnitOfWork интерфейс и сделать каждый класс обслуживания зависимым от него?Как еще я могу разрешить своим классам обслуживания управлять единицей работы?

Ответы [ 2 ]

24 голосов
/ 09 ноября 2010

Вы не должны пытаться подать само AcmeDataContext на EmployeeRepository.Я бы даже перевернул все это:

  1. Определить фабрику, которая позволяет создавать новую единицу работы для домена Acme:
  2. Создать реферат AcmeUnitOfWork, который абстрагируется от LINQв SQL.
  3. Создание конкретной фабрики, которая позволяет создавать новую единицу работ LINQ to SQL.
  4. Регистрация этой конкретной фабрики в конфигурации DI.
  5. Реализация InMemoryAcmeUnitOfWork для модульного тестирования.
  6. Опционально реализуйте удобные методы расширения для общих операций в ваших IQueryable<T> репозиториях.

ОБНОВЛЕНИЕ: я написал сообщение в блоге на эту тему: Подделка вашего провайдера LINQ .

Ниже приведен пошаговый пример с примерами:

ПРЕДУПРЕЖДЕНИЕ. Это будет долгое сообщение.

Шаг 1: Определение фабрики:

public interface IAcmeUnitOfWorkFactory
{
    AcmeUnitOfWork CreateNew();
}

Создание фабрики важно, потому что DataContext реализует IDisposable, поэтому вы хотите владеть экземпляром.В то время как некоторые платформы позволяют вам распоряжаться объектами, когда они больше не нужны, фабрики делают это очень явным.

Шаг 2. Создание абстрактной единицы работы для домена 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) { }
}

некоторые интересные вещи, чтобы отметить об этом абстрактном классе.Единица работы контролирует и создает хранилища.Хранилище - это то, что реализует IQueryable<T>.Репозиторий реализует свойства, которые возвращают конкретный репозиторий.Это препятствует тому, чтобы пользователи вызывали uow.GetRepository<Employee>(), и это создает модель, очень близкую к тому, что вы уже делаете с LINQ to SQL или Entity Framework.

Единица работы реализует операции Insert и Delete,В LINQ to SQL эти операции помещаются в классы Table<T>, но если вы попытаетесь реализовать его таким образом, это не позволит вам абстрагироваться от LINQ до SQL.

Шаг 3. Создайте конкретную фабрику:

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);
    }
}

Фабрика создала LinqToSqlAcmeUnitOfWork на основе базового класса AcmeUnitOfWork:

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(); }
}

Шаг 4. Зарегистрируйте этот конкретный завод в конфигурации DI.

Вы лучше всех знаете, как зарегистрировать интерфейс IAcmeUnitOfWorkFactory для возврата экземпляра LinqToSqlAcmeUnitOfWorkFactory, но это будет выглядеть примерно так:

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

Теперь вы можете изменять зависимости EmployeeService для использования 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();
        }
    }
}

Обратите внимание, что вы даже можете удалить интерфейс IEmployeeService и позволить контроллеру напрямую использовать EmployeeService.Вам не нужен этот интерфейс для модульного тестирования, потому что вы можете заменить единицу работы во время тестирования, предотвращая доступ EmployeeService к базе данных.Это, вероятно, также сэкономит вам массу конфигурации DI, потому что большинство DI-структур знают, как создать конкретный класс.

Шаг 5. Реализация InMemoryAcmeUnitOfWork для модульного тестирования.

Все этиабстракции существуют по причине.Модульное тестирование.Теперь давайте создадим AcmeUnitOfWork для целей модульного тестирования:

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)
    { 
    }
}

Вы можете использовать этот класс в своих модульных тестах.Например:

[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));
}

Шаг 6: При желании реализовать удобные методы расширения:

Ожидается, что репозитории будут иметь удобные методы, такие как GetById или GetByLastName.Конечно, IQueryable<T> является универсальным интерфейсом и не содержит таких методов.Мы могли бы загромождать наш код вызовами типа context.Employees.Single(e => e.Id == employeeId), но это действительно ужасно.Идеальное решение этой проблемы: методы расширения:

// 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);
        }
    }
}

С этими методами расширения можно вызывать эти GetById и другие методы из вашего кода:

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

Что самое приятное в этом коде (я использую его в производстве), так это то, что он сразу избавляет вас от написания большого количества кода для модульного тестирования.Вы обнаружите, что добавляете методы к классу AcmeRepositoryExtensions и свойства к классу AcmeUnitOfWork, когда в систему добавляются новые сущности, но вам не нужно создавать новые классы репозитория для производства или тестирования.

Эта модель, конечно, имеет некоторые недостатки. Возможно, наиболее важным является то, что LINQ to SQL не полностью абстрагируется, потому что вы все еще используете объекты, созданные LINQ to SQL. Эти сущности содержат EntitySet<T> свойства, специфичные для LINQ to SQL. Я не обнаружил, что они мешают надлежащему модульному тестированию, поэтому для меня это не проблема. Если вы хотите, вы всегда можете использовать объекты POCO с LINQ to SQL.

Другим недостатком является то, что сложные запросы LINQ могут успешно пройти тестирование, но не могут быть выполнены из-за ограничений (или ошибок) в поставщике запросов (особенно провайдера запросов EF 3.5 отстой). Когда вы не используете эту модель, вы, вероятно, пишете пользовательские классы репозитория, которые полностью заменяются версиями модульных тестов, и у вас все еще будет проблема невозможности протестировать запросы к вашей базе данных в модульных тестах. Для этого вам понадобятся интеграционные тесты, завернутые транзакцией.

Последним недостатком этой конструкции является использование методов Insert и Delete в единице работы. Хотя перемещение их в хранилище приведет к созданию дизайна с определенным интерфейсом class IRepository<T> : IQueryable<T>, это предотвратит другие ошибки. В решении, которое я использую сам, у меня также есть методы InsertAll(IEnumerable) и DeleteAll(IEnumerable). Однако легко набрать это неправильно и написать что-то вроде context.Delete(context.Messages) (обратите внимание на использование Delete вместо DeleteAll). Это хорошо скомпилируется, потому что Delete принимает object. Конструкция с операциями удаления в хранилище предотвратит компиляцию такого оператора, потому что хранилища напечатаны.

ОБНОВЛЕНИЕ: я написал сообщение в блоге на эту тему, которое описывает это решение более подробно: Подделка вашего провайдера LINQ .

Надеюсь, это поможет.

2 голосов
/ 09 ноября 2010

Если объединить шаблоны единиц работы и репозитория, некоторые люди рекомендуют управлять UoW вне репозитория, чтобы вы могли создать два репозитория (скажем, CustomerRepository и OrderRepository) и передавать им один и тот же экземпляр UoW, гарантирующий, что все изменения БД будет сделан атомарно, когда вы наконец вызовете UoW.Complete ().

Однако в зрелом решении DDD не должно быть необходимости ни в UoW, ни в хранилище. Это потому, что в таком решении совокупные границы определяются таким образом, что нет необходимости в атомарных изменениях, затрагивающих более одного хранилища.

Это отвечает на ваш вопрос?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...