Ed *_*d I 10 asp.net-mvc domain-driven-design dependency-injection inversion-of-control linq-to-sql
我有一个使用ASP.NET MVC,Unity和Linq to SQL的应用程序.
统一寄存器容器的类型AcmeDataContext,从继承System.Data.Linq.DataContext,具有LifetimeManager使用HttpContext.
有一个控制器工厂使用统一容器获取控制器实例.我在构造函数上设置了所有依赖项,如下所示:
// 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
Run Code Online (Sandbox Code Playgroud)
每当需要构造函数时,unity容器就会解析一个连接,用于解析数据上下文,然后是存储库,然后是服务,最后是控制器.
问题是IEmployeeRepository暴露SubmitChanges方法,因为服务类没有DataContext引用.
我被告知应该从存储库外部管理工作单元,所以我似乎应该SubmitChanges从我的存储库中删除它.这是为什么?
如果这是真的,这是否意味着我必须声明一个IUnitOfWork接口并使每个服务类依赖它?我还能如何让我的服务类来管理工作单元?
Ste*_*ven 24
你不应该试图为AcmeDataContext自己提供自己EmployeeRepository.我甚至会把整个事情都转过来:
AcmeUnitOfWork,摘要LINQ to SQL.InMemoryAcmeUnitOfWork单元测试.IQueryable<T>存储库中的常见操作实现方便的扩展方法.更新:我写了一篇关于这个主题的博客文章:伪造你的LINQ提供者.
以下是一些示例:
警告:这将是一个loooong帖子.
第1步:定义工厂:
public interface IAcmeUnitOfWorkFactory
{
AcmeUnitOfWork CreateNew();
}
Run Code Online (Sandbox Code Playgroud)
创建工厂很重要,因为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) { }
}
Run Code Online (Sandbox Code Playgroud)
关于这个抽象类有一些有趣的事情需要注意.工作单元控制并创建存储库.存储库基本上是实现的东西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);
}
}
Run Code Online (Sandbox Code Playgroud)
工厂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(); }
}
Run Code Online (Sandbox Code Playgroud)
第4步:在DI配置中注册具体工厂.
你知道如何注册IAcmeUnitOfWorkFactory接口以返回一个实例LinqToSqlAcmeUnitOfWorkFactory,但它看起来像这样:
container.RegisterSingle<IAcmeUnitOfWorkFactory>(
new LinqToSqlAcmeUnitOfWorkFactory()
{
AcmeConnectionString =
AppSettings.ConnectionStrings["ACME"].ConnectionString
});
Run Code Online (Sandbox Code Playgroud)
现在您可以更改依赖关系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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
请注意,您甚至可以删除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)
{
}
}
Run Code Online (Sandbox Code Playgroud)
您可以在单元测试中使用此类.例如:
[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));
}
Run Code Online (Sandbox Code Playgroud)
第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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
使用这些扩展方法,它允许您GetById从代码中调用这些和其他方法:
var employee = context.Employees.GetById(employeeId);
Run Code Online (Sandbox Code Playgroud)
关于这段代码(我在生产中使用它)最好的事情就是-once到位 - 它可以避免编写大量代码进行单元测试.在将新实体添加到系统时,您会发现自己将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提供者.
我希望这有帮助.
| 归档时间: |
|
| 查看次数: |
4587 次 |
| 最近记录: |