Entity Framework 4.1 y ASP.NET MVC 3

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