Вы не должны пытаться подать само AcmeDataContext
на EmployeeRepository
.Я бы даже перевернул все это:
- Определить фабрику, которая позволяет создавать новую единицу работы для домена Acme:
- Создать реферат
AcmeUnitOfWork
, который абстрагируется от LINQв SQL. - Создание конкретной фабрики, которая позволяет создавать новую единицу работ LINQ to SQL.
- Регистрация этой конкретной фабрики в конфигурации DI.
- Реализация
InMemoryAcmeUnitOfWork
для модульного тестирования. - Опционально реализуйте удобные методы расширения для общих операций в ваших
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 .
Надеюсь, это поможет.