c# - what - entity framework repository pattern dbcontext
Identidad ASP.NET con repositorio y unidad de trabajo (4)
Estoy aprendiendo patrones de repositorio y unidad de trabajo en la aplicación ASP.NET MVC 5 con Entity Framework 6.
Ya había leído una gran cantidad de tutoriales y artículos, pero casi todos son condradictory. Algunos dicen que los patrones de repositorio y unidad de trabajo son buenos, otros dicen que DbContext ya es un repositorio y una unidad de trabajo, otros dicen algo similar, pero ofrecen un enfoque completamente diferente. Probé todos estos enfoques diferentes (bueno, tal vez no todos) y todavía estoy luchando con respecto a cuál enfoque es el más correcto.
Lo que tengo actualmente es:
- IRepository y GenericRepository implementando IRepository
- IUnitOfWork y UnitOfWork implementando IUnitOfWork
- IDbContext y MyDbContext heredados de IdentityDbContext e implementando IDbContext
No estoy seguro de si necesito pegar el código para ello, creo que es bastante genérico y el problema en realidad no es con Repository / UnitOfWork como tal. El problema que tengo es usar clases de Identidad ASP.NET en combinación con mis Repositorios y Unidad de trabajo. Comparto la misma base de datos para membresía y para todos los demás datos, y creo que es un escenario común. No puedo encontrar la buena solución, ¿cómo puedo instanciar las clases de Identidad ASP.NET usando mis repositorios?
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(_DBCONTEXT_);
this.UserManager = new UserManager<ApplicationUser>(store);
¿Qué debería poner en lugar de DBCONTEXT para compartir el mismo DbContext con mi UnitOfWork? ¿O cómo se puede hacer de alguna otra manera para hacer que ASP.NET Identity funcione con UnitOfWork?
Intenté exponer DbContext como propiedad pública de la clase UnitOfWork, algo así como:
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(this.unitOfWork.MyDbContext);
Sin embargo, no creo que sea correcto, no funciona con la interfaz personalizada IDbContext, y hace que el código no sea bueno para las pruebas unitarias.
También traté de implementar CustomUserStore y CustomRoleStore; en general, funcionó, pero cuando lo estaba probando, se requería implementar más y más métodos. Esta solución parece demasiado complicada. Realmente espero que haya una manera más simple.
"Una cuestión a tener en cuenta es que la clase UserStore no funciona bien cuando se usa el patrón de diseño de la unidad de trabajo. Específicamente, la UserStore invoca SaveChanges en casi todas las llamadas a métodos de forma predeterminada, lo que facilita el compromiso prematuro de una unidad de trabajo Para cambiar este comportamiento, cambie el indicador AutoSaveChanges en UserStore ".
var store = new UserStore<ApplicationUser>(new ApplicationDbContext());
store.AutoSaveChanges = false;
De Scott Allen: http://odetocode.com/blogs/scott/archive/2014/01/03/asp-net-identity-with-the-entity-framework.aspx
Encontré algún tipo de solución, que parece bastante genérica, pero todavía no estoy seguro de si es realmente buena y no rompe los principios del patrón Repository / UnitOfWork.
Agregué el método genérico GetDbContext () a mi IUnitOfWork:
public interface IUnitOfWork : IDisposable
{
void Save();
IRepository<TEntity> GetRepository<TEntity>() where TEntity : class;
TContext GetDbContext<TContext>() where TContext : DbContext, IDbContext;
}
Su ''implementación en la clase UnitOfWork:
public class UnitOfWork<TContext> : IUnitOfWork where TContext : IDbContext, new()
{
private IDbContext dbContext;
public UnitOfWork()
{
this.dbContext = new TContext();
}
public T GetDbContext<T>() where T : DbContext, IDbContext
{
return this.dbContext as T;
}
...
}
Cómo se usa en un controlador, inicializando UserManager:
public class AccountController : ControllerBase
{
private readonly IUnitOfWork unitOfWork;
public UserManager<ApplicationUser> UserManager { get; private set; }
public AccountController()
: this(new UnitOfWork<MyDbContext>())
{
}
public AccountController(IUnitOfWork unitOfWork)
{
this.unitOfWork = unitOfWork;
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(unitOfWork.GetDbContext<MyDbContext>());
this.UserManager = new UserManager<ApplicationUser>(store);
}
...
}
Sospecho que GetDbContext () se usará solo para solucionar algunas dificultades con ASP.Identity, por lo que podría no ser tan malo.
He descubierto que trabajar con ASP.Net Identity 2.0 y EF6 es un poco desafiante. El mayor inconveniente es la falta de documentación o documentación conflictiva.
Estoy usando WebApi 2.0, EF6 y ASP.Net Identity 2.0. Al principio fue difícil comenzar, pero una vez que está funcionando, ha sido bueno.
Creé mis propias clases de Identidad. Por el momento no me importa extender las clases de identidad, solo quiero generar las tablas e iniciar sesión en el sistema.
CustomRole
public class CustomRole : IdentityRole<int, CustomUserRole>
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomRole"/> class.
/// </summary>
public CustomRole() { }
/// <summary>
/// Initializes a new instance of the <see cref="CustomRole"/> class.
/// </summary>
/// <param name="name">The name.</param>
public CustomRole(string name) { Name = name; }
}
CustomUserClaim
public class CustomUserClaim : IdentityUserClaim<int> { }
CustomUserLogin
public class CustomUserLogin : IdentityUserLogin<int> { }
CustomUserRole
public class CustomUserRole : IdentityUserRole<int> {}
Usuario
public class User : IdentityUser<int, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
/// <summary>
/// Gets or sets the first name.
/// </summary>
/// <value>The first name.</value>
public string FirstName { get; set; }
/// <summary>
/// Gets or sets the last name.
/// </summary>
/// <value>The last name.</value>
public string LastName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="User"/> is active.
/// </summary>
/// <value><c>true</c> if active; otherwise, <c>false</c>.</value>
public bool Active { get; set; }
}
No me gusta el nombre de las tablas de identidad, así que cambié los nombres.
DataContext
public class DataContext : IdentityDbContext<User, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>
{
public DataContext() : base("DefaultConnection"){}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<CustomUserRole>().ToTable("UserRoles", "Security");
modelBuilder.Entity<CustomUserLogin>().ToTable("UserLogins", "Security");
modelBuilder.Entity<CustomUserClaim>().ToTable("UserClaims", "Security");
modelBuilder.Entity<CustomRole>().ToTable("Roles", "Security");
modelBuilder.Entity<User>().ToTable("Users", "Security");
}
}
Me pareció un poco molesto el UserManager.
Creé una clase estática para manejarlo. La UserStore maneja el ciclo de vida de DataContext, pero tendrá que llamar a disponer para que esto suceda. Esto podría causar problemas si está usando esta referencia de DataContext en otro lugar. Eventualmente lo conectaré a mi contenedor DI, pero por ahora esto es lo que tengo:
public class Identity
{
/// <summary>
/// Gets the user manager.
/// </summary>
/// <returns>UserManager<User, System.Int32>.</returns>
public static UserManager<User, int> GetUserManager()
{
var store = new UserStore<User, CustomRole, int, CustomUserLogin, CustomUserRole, CustomUserClaim>(new DataContext());
var userManager = new UserManager<User, int>(store);
return userManager;
}
}
Uso el patrón de unidad de trabajo para la mayoría de mis datos de acceso. Funciona bien Hay algunos casos en los que tengo datos que necesitan más control de los que expone la unidad de trabajo para estos casos expuse el DataContext. Si eso todavía no funciona para mí, recurriré al uso de un repositorio.
public class UnitOfWork : IUnitOfWork
{
private readonly IContainer _container;
public UnitOfWork(IContainer container) :this()
{
_container = container;
}
//private readonly List<CommitInterception> _postInterceptions = new List<CommitInterception>();
public DataContext Context { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="UnitOfWork"/> class.
/// </summary>
public UnitOfWork()
{
Context = new DataContext();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <exception cref="System.NotImplementedException"></exception>
public void Dispose()
{
//Chuck was here
try
{
Commit();
}
finally
{
Context.Dispose();
}
}
/// <summary>
/// Begins the transaction.
/// </summary>
/// <returns>IUnitOfWorkTransaction.</returns>
public IUnitOfWorkTransaction BeginTransaction()
{
return new UnitOfWorkTransaction(this);
}
/// <summary>
/// Commits this instance.
/// </summary>
public void Commit()
{
Commit(null);
}
/// <summary>
/// Commits transaction.
/// </summary>
public void Commit(DbContextTransaction transaction)
{
//Lee was here.
try
{
Context.SaveChanges();
if (transaction != null)
{
transaction.Commit();
}
//foreach (var interception in _postInterceptions)
//{
// interception.PostCommit(interception.Instance, this);
//}
}
catch (DbEntityValidationException ex)
{
var errors = FormatError(ex);
throw new Exception(errors, ex);
}
catch
{
if (transaction != null)
{
transaction.Rollback();
}
throw;
}
finally
{
// _postInterceptions.Clear();
}
}
/// <summary>
/// Formats the error.
/// </summary>
/// <param name="ex">The ex.</param>
/// <returns>System.String.</returns>
private static string FormatError(DbEntityValidationException ex)
{
var build = new StringBuilder();
foreach (var error in ex.EntityValidationErrors)
{
var errorBuilder = new StringBuilder();
foreach (var validationError in error.ValidationErrors)
{
errorBuilder.AppendLine(string.Format("Property ''{0}'' errored:{1}", validationError.PropertyName, validationError.ErrorMessage));
}
build.AppendLine(errorBuilder.ToString());
}
return build.ToString();
}
/// <summary>
/// Inserts the specified entity.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="entity">The entity.</param>
/// <returns>``0.</returns>
public T Insert<T>(T entity) where T: class
{
var instance = _container.TryGetInstance<IUnitOfWorkInterception<T>>();
if (instance != null)
{
instance.Intercept(entity, this);
// _postInterceptions.Add(new CommitInterception() { Instance = entity, PostCommit = (d,f) => instance.PostCommit(d as T, f) });
}
var set = Context.Set<T>();
var item = set.Add(entity);
return item;
}
public T Update<T>(T entity) where T : class
{
var set = Context.Set<T>();
set.Attach(entity);
Context.Entry(entity).State = EntityState.Modified;
return entity;
}
/// <summary>
/// Deletes the specified entity.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="entity">The entity.</param>
public void Delete<T>(T entity) where T : class
{
var set = Context.Set<T>();
set.Remove(entity);
}
/// <summary>
/// Finds the specified predicate.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="predicate">The predicate.</param>
/// <returns>IQueryable{``0}.</returns>
public IQueryable<T> Find<T>(Expression<Func<T, bool>> predicate) where T : class
{
var set = Context.Set<T>();
return set.Where(predicate);
}
/// <summary>
/// Gets all.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>IQueryable{``0}.</returns>
public IQueryable<T> GetAll<T>() where T : class
{
return Context.Set<T>();
}
/// <summary>
/// Gets the by identifier.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="id">The identifier.</param>
/// <returns>``0.</returns>
public T GetById<T>(int id) where T : class
{
var set = Context.Set<T>();
return set.Find(id);
}
/// <summary>
/// Executes the query command.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sql">The SQL.</param>
/// <returns>DbSqlQuery{``0}.</returns>
public DbSqlQuery<T> ExecuteQueryCommand<T>(string sql) where T : class
{
var set = Context.Set<T>();
return set.SqlQuery(sql);
}
private class CommitInterception
{
public object Instance { get; set; }
public Action<object, IUnitOfWork> PostCommit { get; set; }
}
}
public class UnitOfWorkTransaction : IUnitOfWorkTransaction
{
private readonly UnitOfWork _unitOfWork;
private readonly DbContextTransaction _transaction;
/// <summary>
/// Initializes a new instance of the <see cref="UnitOfWorkTransaction"/> class.
/// </summary>
/// <param name="unitOfWork">The unit of work.</param>
public UnitOfWorkTransaction(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_transaction = _unitOfWork.Context.Database.BeginTransaction();
Context = unitOfWork.Context;
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
_unitOfWork.Commit(_transaction);
}
public DataContext Context { get; set; }
/// <summary>
/// Commits this instance.
/// </summary>
public void Commit()
{
_unitOfWork.Commit();
}
/// <summary>
/// Rollbacks this instance.
/// </summary>
public void Rollback()
{
_transaction.Rollback();
}
/// <summary>
/// Inserts the specified entity.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="entity">The entity.</param>
/// <returns>T.</returns>
public T Insert<T>(T entity) where T : class
{
return _unitOfWork.Insert(entity);
}
/// <summary>
/// Updates the specified entity.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="entity">The entity.</param>
/// <returns>T.</returns>
public T Update<T>(T entity) where T : class
{
return _unitOfWork.Update(entity);
}
/// <summary>
/// Deletes the specified entity.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="entity">The entity.</param>
public void Delete<T>(T entity) where T : class
{
_unitOfWork.Delete(entity);
}
/// <summary>
/// Finds the specified predicate.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="predicate">The predicate.</param>
/// <returns>IQueryable<T>.</returns>
public IQueryable<T> Find<T>(Expression<Func<T, bool>> predicate) where T : class
{
return _unitOfWork.Find(predicate);
}
/// <summary>
/// Gets all.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns>IQueryable<T>.</returns>
public IQueryable<T> GetAll<T>() where T : class
{
return _unitOfWork.GetAll<T>();
}
/// <summary>
/// Gets the by identifier.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="id">The identifier.</param>
/// <returns>T.</returns>
public T GetById<T>(int id) where T : class
{
return _unitOfWork.GetById<T>(id);
}
/// <summary>
/// Executes the query command.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sql">The SQL.</param>
/// <returns>DbSqlQuery<T>.</returns>
public DbSqlQuery<T> ExecuteQueryCommand<T>(string sql) where T : class
{
return _unitOfWork.ExecuteQueryCommand<T>(sql);
}
}
Aquí hay algunos ejemplos de esto en acción. Tengo un fondo nHibernate y me gusta definir una transacción en el ámbito de un using
así que lo implementé en mi unidad de trabajo.
using (var trans = _unitOfWork.BeginTransaction())
{
var newAgency = trans.Insert(new Database.Schema.Agency() { Name = agency.Name, TaxId = agency.TaxId });
}
Otro ejemplo de usar el "Buscar" fuera de la Unidad de trabajo:
var users = _unitOfWork.Find<Database.Schema.User>(s => s.Active && s.Agency_Id == agencyId)
.Select(u=> new {Label = u.FirstName + " " + u.LastName, Value = u.Id})
.ToList();
Creación de usuario e inicio de sesión de usuario
Uso ASP.NET Identity para el inicio de sesión y la creación del usuario y mi Unidad de trabajo para todo lo demás.
Pruebas
No trataría de probar ASP.NET Identity. Por un lado, estoy seguro de que Microsoft hizo un buen trabajo al probarlo. Estoy seguro de que hicieron un mejor trabajo del que tú o yo podríamos hacer. Si realmente desea probar el código de identidad de ASP.NET, póngalo detrás de una interfaz y burlarse de la interfaz.
Si está usando el patrón Repository y UnitofWork, puede estar utilizándolo con DDD (Domain Driven Design) donde declara IRepository o IUnitofWork en el proyecto Core junto con el resto del modelo de dominio y las clases abstractas.
Ahora crea un proyecto de Infraestructura que implemente esas interfaces en el proyecto Core usando un objeto concreto de acceso a datos para esta instancia Entity Framework. así que DbContext está bien allí, pero sí, no lo expongas a la capa de presentación. Entonces, en algún momento, si desea cambiar EF a cualquier otro ORM, será más fácil sin tocar la capa de presentación donde coloca sus clases de Identidad separadas del proyecto de Acceso a datos o Infraestructura. Y, por supuesto, puede usar el contenedor IOC para crear instancias de los Repositorios concretos de la infraestructura en los controladores de la capa de presentación.