Con la salida de EF 4.1 ha vuelto la discusión sobre el manejo de la Unit of Work en una aplicación web. En este caso vamos a ver algunos pasos utilizando EF 4.1, MVC 3 y StructureMap.
Paso 1: El Dominio
public abstract class Entity
{
public int Id { get; set; }
}
public class Post:Entity
{
public String Title { get; set; }
public String Body { get; set; }
public List<Comment> Comments { get; set; }
}
public class Comment:Entity
{
public string Content { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
}
Paso 2: DbContext y la cadena de conexión
Creamos una clase que extienda de DbContext para que a través de ella interactuemos con la base de datos. Por defecto busca una cadena de conexión que tenga el mismo nombre de la clase que lo implementa, en este caso una cadena con el nombre de DatabaseContext. Podemos cambiar el nombre de esta cadena si ingresamos un nuevo parámetro al constructor del DbContext.
public class DatabaseContext : DbContext
{
public DatabaseContext() : base("Blog") {}
}
<add name="Blog" connectionString="xXx" providerName="System.Data.SqlClient" />
Paso 3: Los Repositorios
No me gusta la idea de tener todos los DbSet dentro del DatabaseContext, es por esto que creamos un repositorio base que instancie un DbSet para su correspondiente entidad y también agregamos algunos métodos comunes.
public class Repository<T> where T : Entity
{
public Repository()
{
var database = ObjectFactory.GetInstance<DatabaseContext>();
this.Set = database.Set<T>();
}
protected readonly DbSet<T> Set;
public IEnumerable<T> All()
{
return this.Set.ToList();
}
public void Add(T entity)
{
this.Set.Add(entity);
}
public T Get(int id)
{
return this.Set.SingleOrDefault(x=>x.Id==id);
}
public void Delete(T entity)
{
this.Set.Remove(entity);
}
}
En este caso no estamos inyectando el DatabaseContext por el constructor sino lo obtenemos a través de un Service Locator, esto con la finalidad de no tener que escribir el constructor en todos los repositorios hijos.
public interface IPostRepository
{
Post Get(int id);
void Add(Post post);
void Delete(Post post);
IEnumerable<Post> All();
IEnumerable<Comment> Comments(int idPost);
}
public class PostRepository : Repository<Post>, IPostRepository
{
public IEnumerable<Comment> Comments(int idPost)
{
return this.Set.SelectMany(p => p.Comments)
.Where(c => c.Post.Id == idPost).ToList();
}
}
Paso 4: Inyección de Dependencias
En ASP.NET MVC 3 se ha formalizado la inyección de dependencias, es por esto que podemos implementar la nueva interfaz IDependencyResolver e indicarle a MVC que utilice esta clase para determinar cuál es la instancia correcta cuando se solicite un determinado tipo.
public class StructureMapDependencyResolver:IDependencyResolver
{
public StructureMapDependencyResolver(IContainer container)
{
this.container = container;
}
private readonly IContainer container;
public object GetService(Type serviceType)
{
if (serviceType.IsAbstract || serviceType.IsInterface)
{
return this.container.TryGetInstance(serviceType);
}
return this.container.GetInstance(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return container.GetAllInstances<object>()
.Where(s => s.GetType() == serviceType);
}
}
Ahora configuramos StructureMap y registramos la clase anterior dentro del Global.asax. El punto más importante en cuanto al manejo de la unidad de trabajo es configurar el contenedor para que se tenga una única instancia del DatabaseContext a nivel de todo un request.
protected void Application_Start()
{
ObjectFactory.Initialize(x =>
{
x.For<DatabaseContext>().HybridHttpOrThreadLocalScoped();
x.For<IPostRepository>().Use<PostRepository>();
});
DependencyResolver.SetResolver(
new StructureMapDependencyResolver(ObjectFactory.Container));
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Si utilizamos StructureMap necesitamos limpiar todos los objetos que tienen un ciclo de vida a nivel de request, en este caso el DatabaseContext.
protected void Application_EndRequest(object sender, EventArgs e)
{
ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
}
Paso 4: El Filtro de Transacción
Ahora que se comparte el mismo DatabaseContext en todo un request, cuando este finalice, necesitamos enviar a la base de datos todos los cambios que se hayan realizado como si fueran una sola unidad o transacción. Para esto creamos un filtro de acción que guarde los cambios realizados dentro del DatabaseContext.
public class TransactionAttribute : ActionFilterAttribute
{
public DatabaseContext Context { get; set; }
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext.Exception == null)
{
try
{
Context.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
//manejar errores de concurrencia como sea conveniente
}
catch (Exception)
{
//manejar errores genéricos como sea conveniente
}
}
}
}
Para poder inyectar la instancia correcta del DatabaseContext dentro del filtro, necesitamos extender la clase FilterAttributeFilterProvider.
public class StructureMapFilterProvider : FilterAttributeFilterProvider
{
private readonly IContainer container;
public StructureMapFilterProvider(IContainer container)
{
this.container = container;
}
public override IEnumerable<Filter> GetFilters
(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
var filters = base.GetFilters(controllerContext, actionDescriptor);
foreach (var filter in filters)
{
container.BuildUp(filter.Instance);
}
return filters;
}
}
Modificamos el Global.asax para registrar la clase anterior e indicarle a StructureMap que realice inyección a través de propiedades para el DatabaseContext.
protected void Application_Start()
{
ObjectFactory.Initialize(x =>
{
x.For<DatabaseContext>().HybridHttpOrThreadLocalScoped();
x.For<IPostRepository>().Use<PostRepository>();
x.SetAllProperties(p => p.OfType<DatabaseContext>());
});
DependencyResolver.SetResolver(
new StructureMapDependencyResolver(ObjectFactory.Container));
FilterProviders.Providers.Add(
new StructureMapFilterProvider(ObjectFactory.Container));
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
Paso 5: Registrando las entidades en el DatabaseContext
Debido a que los DbSets de nuestras entidades no se encuentran dentro del DatabaseContext, este no las reconoce para poder utilizarlas; para registrarlas debemos sobrescribir el método OnModelCreating y por cada entidad llamar al método genérico modelbuilder.Entity<T>.
public class DatabaseContext : DbContext
{
public DatabaseContext() : base("Blog"){}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var method = typeof(DbModelBuilder).GetMethod("Entity");
var types = Assembly.GetExecutingAssembly().GetExportedTypes()
.Where(t => typeof(Entity).IsAssignableFrom(t) &&
!t.IsAbstract && !t.IsInterface)
.ToList();
foreach (var type in types)
{
var genericMethod = method.MakeGenericMethod(new[] { type });
genericMethod.Invoke(modelBuilder, null);
}
}
}
Paso 6: Utilizando lo anterior en los Controllers
Creamos un controller y algunas acciones que utilicen los repositorios y el filtro anteriormente creado.
public class PostsController : Controller
{
private readonly IPostRepository postRepository;
public PostsController(IPostRepository postRepository)
{
this.postRepository = postRepository;
}
public ActionResult Index()
{
IEnumerable<Post> posts = postRepository.All();
return View(posts);
}
public ActionResult Create()
{
return View();
}
[HttpPost]
[Transaction]
public ActionResult Create(Post post)
{
postRepository.Add(post);
return RedirectToAction("index");
}
}
Último Paso: Creando la Base de datos
Por último podemos crear la base de datos y sus tablas de forma manual o dejar que estas se creen automáticamente cuando se instancie por primera vez el DatabaseContext.
Con esto ya tenemos todo lo necesario para el manejo de la unidad de trabajo a nivel de request.
Hasta el siguiente post.
Saludos
Angel Núñez Salazar