您不应尝试将AcmeDataContext 本身提供给EmployeeRepository。我什至会扭转整个局面:
- 定义一个允许为 Acme 域创建新工作单元的工厂:
- 创建一个抽象的
AcmeUnitOfWork 抽象出LINQ to SQL。
- 创建一个具体工厂,允许创建新的 LINQ to SQL 工作单元。
- 在您的 DI 配置中注册该具体工厂。
- 为单元测试实现
InMemoryAcmeUnitOfWork。
- 可选择为您的
IQueryable<T> 存储库上的常见操作实施方便的扩展方法。
更新:我写了一篇关于这个主题的博文:Faking your LINQ provider。
下面是一步一步的例子:
警告:这将是一个很长的帖子。
第 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 或实体框架所做的模型。
工作单元实现Insert 和Delete 操作。在 LINQ to SQL 中,这些操作放在 Table<T> 类上,但是当您尝试以这种方式实现它时,它会阻止您将 LINQ to 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);
}
}
工厂基于AcmeUnitOfWork基类创建了LinqToSqlAcmeUnitOfWork:
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 生成的实体。这些实体包含特定于 LINQ to SQL 的 EntitySet<T> 属性。我没有发现它们妨碍正确的单元测试,所以对我来说这不是问题。如果您愿意,您可以随时将 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。对存储库进行删除操作的设计会阻止此类语句编译,因为存储库是类型化的。
更新:我写了一篇关于这个主题的博文,更详细地描述了这个解决方案:Faking your LINQ provider。
我希望这会有所帮助。