Cómo eliminar la funcionalidad de la unidad de trabajo de los repositorys usando IOC

Tengo una aplicación que usa ASP.NET MVC, Unity y Linq to SQL.

El contenedor de unidad registra el tipo AcmeDataContext que hereda de System.Data.Linq.DataContext , con un LifetimeManager utiliza HttpContext .

Hay una fábrica de controladores que obtiene las instancias del controlador utilizando el contenedor de unidades. Configuré todas mis dependencias en los constructores, así:

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

Cada vez que se necesita un constructor, el contenedor de unidad resuelve una conexión, que se usa para resolver un contexto de datos, luego un repository, luego un servicio y finalmente el controlador.

El problema es que IEmployeeRepository expone el método SubmitChanges , ya que las clases de servicio NO tienen una referencia de DataContext .

Me han dicho que la unidad de trabajo debe gestionarse desde fuera de los repositorys, por lo que parece que debería eliminar SubmitChanges de mis repositorys. ¿Porqué es eso?

Si esto es cierto, ¿significa esto que tengo que declarar una interfaz IUnitOfWork y hacer que cada clase de servicio dependa de ella? ¿De qué otra manera puedo permitir que mis clases de servicio administren la unidad de trabajo?

No intente suministrar el AcmeDataContext sí mismo al EmployeeRepository . Incluso podría darle la vuelta a todo esto:

  1. Defina una fábrica que permita crear una nueva unidad de trabajo para el dominio Acme:
  2. Cree un AcmeUnitOfWork abstracto que AcmeUnitOfWork LINQ to SQL.
  3. Cree una fábrica de concreto que pueda crear nuevas unidades de trabajo de LINQ a SQL.
  4. Registre esa fábrica de concreto en su configuración DI.
  5. Implemente InMemoryAcmeUnitOfWork para pruebas unitarias.
  6. Opcionalmente, implemente métodos de extensión convenientes para operaciones comunes en sus repositorys IQueryable .

ACTUALIZACIÓN: escribí una publicación en el blog sobre este tema: falsificar su proveedor de LINQ .

A continuación hay un paso a paso con ejemplos:

ADVERTENCIA: Esta será una gran publicación.

Paso 1: Definiendo la fábrica:

 public interface IAcmeUnitOfWorkFactory { AcmeUnitOfWork CreateNew(); } 

Crear una fábrica es importante, porque DataContext implementa IDisposable por lo que desea tener la propiedad de la instancia. Si bien algunos marcos te permiten disponer de objetos cuando ya no los necesites, las fábricas lo hacen muy explícito.

Paso 2: crear una unidad de trabajo abstracta para el dominio Acme:

 public abstract class AcmeUnitOfWork : IDisposable { public IQueryable Employees { [DebuggerStepThrough] get { return this.GetRepository(); } } public IQueryable Orders { [DebuggerStepThrough] get { return this.GetRepository(); } } 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 GetRepository() where T : class; protected virtual void Dispose(bool disposing) { } } 

Hay algunas cosas interesantes para tener en cuenta sobre esta clase abstracta. La Unidad de trabajo controla y crea los repositorys. Un repository es básicamente algo que implementa IQueryable . El repository implementa propiedades que devuelven un repository específico. Esto evita que los usuarios uow.GetRepository() a uow.GetRepository() y esto crea un modelo que está muy cerca de lo que ya está haciendo con LINQ to SQL o Entity Framework.

La unidad de trabajo implementa las operaciones Insert y Delete . En LINQ to SQL, estas operaciones se ubican en las clases Table , pero cuando intenta implementarlo de esta forma, evitará que abstraiga LINQ to SQL.

Paso 3. Crea una fábrica de concreto:

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

La fábrica creó un LinqToSqlAcmeUnitOfWork basado en la clase base 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 GetRepository() where TEntity : class { return this.db.GetTable(); } protected override void Dispose(bool disposing) { this.db.Dispose(); } } 

Paso 4: Registre esa fábrica de concreto en su configuración DI.

Usted sabe mejor que IAcmeUnitOfWorkFactory cómo registrar la interfaz IAcmeUnitOfWorkFactory para devolver una instancia de LinqToSqlAcmeUnitOfWorkFactory , pero se vería más o menos así:

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

Ahora puede cambiar las dependencias en EmployeeService para usar 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(); } } } 

Tenga en cuenta que incluso podría eliminar la interfaz IEmployeeService y dejar que el controlador use el EmployeeService directamente. No necesita esta interfaz para las pruebas unitarias, ya que puede reemplazar la unidad de trabajo durante las pruebas y evitar que EmployeeService acceda a la base de datos. Esto probablemente también le ahorrará mucha configuración DI, porque la mayoría de los marcos DI saben cómo crear una instancia de una clase concreta.

Paso 5: Implemente InMemoryAcmeUnitOfWork para pruebas unitarias.

Todas estas abstracciones están ahí por una razón. Examen de la unidad. Ahora vamos a crear un AcmeUnitOfWork para propósitos de pruebas unitarias:

 public class InMemoryAcmeUnitOfWork: AcmeUnitOfWork, IAcmeUnitOfWorkFactory { private readonly List committed = new List(); private readonly List uncommittedInserts = new List(); private readonly List uncommittedDeletes = new List(); // 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 Committed() where TEntity : class { return this.committed.OfType(); } protected override IQueryable GetRepository() { // Only return committed objects. Same behavior as L2S and EF. return this.committed.OfType().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) { } } 

Puedes usar esta clase en tus pruebas unitarias. Por ejemplo:

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

Paso 6: Implemente opcionalmente métodos de extensión convenientes:

Se espera que los repositorys tengan métodos convenientes como GetById o GetByLastName . Por supuesto IQueryable es una interfaz genérica y no contiene tales métodos. Podríamos atestar nuestro código con llamadas como context.Employees.Single(e => e.Id == employeeId) , pero eso es realmente feo. La solución perfecta a este problema es: métodos de extensión:

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

Con estos métodos de extensión implementados, le permite llamar a esos GetById y a otros métodos desde su código:

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

Lo mejor de este código (lo uso en producción) es que, una vez en su lugar, le ahorra escribir un montón de código para las pruebas unitarias. Se encontrará añadiendo métodos a la clase AcmeRepositoryExtensions y propiedades a la clase AcmeUnitOfWork cuando se agreguen nuevas entidades al sistema, pero no es necesario crear nuevas clases de repository para producción o prueba.

Este modelo tiene, por supuesto, algunos shortcomes. Quizás lo más importante es que LINQ to SQL no se abstrae completamente, porque aún se usan las entidades LINQ to SQL. Esas entidades contienen EntitySet que son específicas de LINQ to SQL. No he encontrado que interfieran con las pruebas unitarias correctas, así que para mí no es un problema. Si lo desea, siempre puede usar objetos POCO con LINQ to SQL.

Otra ventaja es que las consultas LINQ complicadas pueden tener éxito en la prueba pero fallan en la producción, debido a las limitaciones (o errores) en el proveedor de consultas (especialmente el proveedor de consultas EF 3.5 apesta). Cuando no utiliza este modelo, probablemente esté escribiendo clases de repository personalizadas que son completamente reemplazadas por versiones de prueba de unidad y todavía tendrá el problema de no poder probar las consultas a su base de datos en pruebas unitarias. Para esto, necesitará pruebas de integración, envueltas en una transacción.

Una última ventaja de este diseño es el uso de métodos Insert y Delete en la Unidad de trabajo. Si bien moverlos al repository lo obligaría a tener un diseño con una class IRepository : IQueryable específica, le evitará otros errores. En la solución que uso, también tengo los InsertAll(IEnumerable) y DeleteAll(IEnumerable) . Sin embargo, es fácil confundir esto y escribir algo como context.Delete(context.Messages) (tenga en cuenta el uso de Delete lugar de DeleteAll ). Esto comstackría bien, porque Delete acepta un object . Un diseño con operaciones de eliminación en el repository evitaría que dicha statement se comstackra, ya que los repositorys se escriben a máquina.

ACTUALIZACIÓN: escribí una publicación de blog sobre este tema que describe esta solución con más detalle: falsificando su proveedor de LINQ .

Espero que esto ayude.

Si combina la unidad de trabajo y los patrones del repository, algunas personas recomiendan que UoW se administre fuera del repository para poder crear dos repositorys (por ejemplo, CustomerRepository y OrderRepository) y pasarles la misma instancia de UoW asegurando que todos los cambios en el DB hacerse atómicamente cuando finalmente llame a UoW.Complete ().

Sin embargo, en una solución madura de DDD, no debería haber necesidad de UoW y un repository. Esto se debe a que tales límites agregados de una solución se definen de tal manera que no hay necesidad de cambios atómicos que involucren a más de un repository.

¿Responde esto a tu pregunta?