带有存储库和工作单元的 ASP.NET 标识

Posted

技术标签:

【中文标题】带有存储库和工作单元的 ASP.NET 标识【英文标题】:ASP.NET Identity with Repository and Unit of Work 【发布时间】:2014-06-07 05:08:42 【问题描述】:

我正在使用 Entity Framework 6 学习 ASP.NET MVC 5 应用程序中的存储库和工作单元模式。

我已经阅读了很多教程和文章,但几乎所有的都是自相矛盾的。有人说存储库和工作单元模式很好,其他人说 DbContext 已经是一个存储库和工作单元,其他人说类似的东西,但提供了完全不同的方法。我尝试了所有这些不同的方法(嗯,也许不是全部),但仍在为哪种方法最正确而苦苦挣扎。

我目前拥有的是:

IRepository 和 GenericRepository 实现 IRepository IUnitOfWork 和 UnitOfWork 实现 IUnitOfWork IDbContext 和 MyDbContext 继承自 IdentityDbContext 并实现 IDbContext

不确定我是否需要为其粘贴代码,我认为它非常通用,问题实际上不在于 Repository/UnitOfWork 本身。我遇到的问题是将 ASP.NET 标识类与我的存储库和工作单元结合使用。 我为会员资格和所有其他数据共享同一个数据库——我认为这是一个常见的场景。我找不到好的解决方案如何使用我的存储库实例化 ASP.NET 标识类。

UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(_DBCONTEXT_);
this.UserManager = new UserManager<ApplicationUser>(store);

我应该用什么代替 DBCONTEXT,以便它与我的 UnitOfWork 共享相同的 DbContext?或者如何以其他方式使 ASP.NET Identity 与 UnitOfWork 一起工作?

我尝试将 DbContext 公开为 UnitOfWork 类的公共属性,例如:

UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(this.unitOfWork.MyDbContext);

但是我认为它不正确 - 它不适用于自定义 IDbContext 接口,并且使代码不适合单元测试。

我也尝试实现 CustomUserStore 和 CustomRoleStore - 通常它可以工作,但是当我测试它时,它需要实现越来越多的方法。这个解决方案看起来太复杂了——我真的希望有更简单的方法。

【问题讨论】:

DbContexta 存储库 - 如果您选择将其用作 the 存储库,那么您将与 EF 紧密耦合并防止任何类型对需要存储库的类进行单元测试。 我觉得这篇文章很清楚asp.net/mvc/tutorials/getting-started-with-ef-5-using-mvc-4/… 这是我浏览的第一篇文章。它真的很有帮助,而且写得很好,但是.. 关于如何使它与 ASP.NET Identity 一起工作的主要问题,我没有提到任何内容。最后我不得不为 UserManager 传递一个单独的 DbContext 实例,这与 UnitOfWork 的概念冲突。 如果您的目标是纯粹的教育(学习存储库/UoW),我会考虑忽略身份并将其视为特例。这里 UserStoreDbContext 有依赖关系,这使得将 EF 与主应用程序完全分离变得很困难。 我发现了另一篇关于基于身份扩展性使用存储库和工作单元与 ASP.NET 身份的好文章:https://timschreiber.com/2015/01/14/persistence-ignorant-asp-net-identity-with-patterns-part-1/。如果链接失效,可以通过GitHub 获得示例 【参考方案1】:

我发现使用 ASP.Net Identity 2.0 和 EF6 有点挑战性。最大的缺点是缺乏文档或文档冲突。

我正在使用 WebApi 2.0、EF6 和 ASP.Net Identity 2.0。起初很难开始,但一旦开始工作,一切都很好。

我创建了自己的身份类。目前我并不关心扩展身份类,我只想生成表并登录到系统。

自定义角色

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>  

自定义用户登录

public class CustomUserLogin : IdentityUserLogin<int>  

自定义用户角色

public class CustomUserRole : IdentityUserRole<int> 

用户

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; 


我不喜欢 Identity 表的命名,所以我更改了名称。

数据上下文

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

    

我发现获取 UserManager 有点痛苦。

我创建了一个静态类来处理它。 UserStore 确实处理 DataContext 的生命周期,但您必须调用 dispose 才能发生这种情况。如果您在其他地方使用此 DataContext 引用,这可能会导致问题。我最终会将它连接到我的 DI 容器中,但现在这就是我所拥有的:

public class Identity

    /// <summary>
    /// Gets the user manager.
    /// </summary>
    /// <returns>UserManager&lt;User, System.Int32&gt;.</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;
    

我使用工作单元模式进行大部分数据访问。它运作良好。在某些情况下,我的数据需要比工作单元公开更多的控制,因为这些情况我公开了 DataContext。如果这仍然对我不起作用,我将回退到使用存储库。

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&lt;T&gt;.</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&lt;T&gt;.</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&lt;T&gt;.</returns>
    public DbSqlQuery<T> ExecuteQueryCommand<T>(string sql) where T : class
    
       return _unitOfWork.ExecuteQueryCommand<T>(sql);
    

这里有几个例子。我有 nHibernate 背景,喜欢在 using 范围内定义事务,所以我在我的工作单元中实现了。

        using (var trans = _unitOfWork.BeginTransaction())
        
            var newAgency = trans.Insert(new Database.Schema.Agency()  Name = agency.Name, TaxId = agency.TaxId );

        

在工作单元之外使用“查找”的另一个示例:

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

用户创建和用户登录

我使用 ASP.NET 身份进行登录和用户创建,我的工作单元用于其他一切。

测试

我不会尝试测试 ASP.NET Identity。一方面,我确信微软在测试它方面做得很好。我敢肯定他们做得比你或我做得更好。如果您真的想测试 ASP.NET 身份代码,请将其放在接口后面并模拟接口。

【讨论】:

【参考方案2】:

“需要注意的一个问题是,UserStore 类在使用工作单元设计模式时表现不佳。具体而言,UserStore 在默认情况下几乎在每个方法调用中都会调用 SaveChanges,这很容易过早地提交工作单元。要更改此行为,请更改 UserStore 上的 AutoSaveChanges 标志。"

var store = new UserStore<ApplicationUser>(new ApplicationDbContext());
store.AutoSaveChanges = false;

来自斯科特·艾伦:http://odetocode.com/blogs/scott/archive/2014/01/03/asp-net-identity-with-the-entity-framework.aspx

【讨论】:

试图在没有这个简单修复的情况下工作是个脑残。【参考方案3】:

找到了某种解决方案,看起来足够通用,但我仍然不确定它是否真的很好并且不会违反 Repository/UnitOfWork 模式原则。

我在 IUnitOfWork 中添加了通用 GetDbContext() 方法:

public interface IUnitOfWork : IDisposable

   void Save();    
   IRepository<TEntity> GetRepository<TEntity>() where TEntity : class;    
   TContext GetDbContext<TContext>() where TContext : DbContext, IDbContext;

它在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;
    

    ...

如何在Controller中使用,初始化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);
    

    ...

我怀疑 GetDbContext() 将用于解决 ASP.Identity 的一些问题,所以它可能不是那么糟糕..

【讨论】:

【参考方案4】:

如果您使用 Repository 和 UnitofWork 模式,您可能会将其与 DDD(域驱动设计)一起使用,您在 Core 项目 中声明 IRepository 或 IUnitofWork 以及所有其他域模型和抽象类。

现在您制作 Infrastructure 项目,它使用此实例实体框架的具体数据访问对象在 Core 项目中实现这些接口。所以 DbContext 在那里很好,但是不要将它暴露给表示层。因此,在某些时候,如果您想将 EF 更改为任何其他 ORM,那么无需触及您将身份类与数据访问或基础设施项目分开的表示层会更容易。当然,您可以使用 IOC 容器从表示层控制器中的基础设施实例化那些具体的存储库。

【讨论】:

以上是关于带有存储库和工作单元的 ASP.NET 标识的主要内容,如果未能解决你的问题,请参考以下文章

asp.net mvc 怎样搭建业务逻辑层比较好?

ASP.NET Core - 环境为 Stage 时找不到嵌入式资源(Microsoft 标识)。它在开发时工作正常

asp.net core, Ef core : 在运行时动态映射存储库和服务

带有标识 2 和 EntityFramework 6(Oracle)的 ASP.NET Core MVC

在 ASP.NET MVC 3 中的多个服务之间共享一个工作单元

如何在 ASP MVC 中实现工作单元、存储库和业务逻辑?