Подтвердить что ты не робот

Идентификация ASP.NET с хранилищем и блоком работы

Я изучаю образцы репозитория и единицы работы в приложении ASP.NET MVC 5 с Entity Framework 6.

Я уже прочитал много учебников и статей, но почти все из них являются кондективными. Говорят, что шаблоны репозитория и единицы работы хороши, другие говорят, что DbContext уже является хранилищем и единицей работы, другие говорят что-то похожее, но предлагают совершенно другой подход. Я пробовал все эти разные подходы (ну, может быть, не все) и все еще борется за то, какой подход является наиболее правильным.

В настоящее время у меня есть:

  • IRepository и GenericRepository, реализующие IRepository
  • IUnitOfWork и UnitOfWork для реализации IUnitOfWork
  • IDbContext и MyDbContext, унаследованные от IdentityDbContext и реализующие IDbContext

Не уверен, что мне нужно вставить код для него, я думаю, что он довольно общий, и проблема на самом деле не связана с репозиторием /UnitOfWork как таковым. Проблема, с которой я сталкиваюсь, заключается в использовании классов Identity ASP.NET в сочетании с моими репозиториями и подразделением работы. Я использую одну и ту же базу данных для членства и для всех других данных - и я считаю это общим сценарием. Я не могу найти хорошее решение, как я могу создать экземпляры классов ASP.NET Identity с помощью своих репозиториев.

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

Что я должен поставить вместо DBCONTEXT, чтобы он делил один и тот же DbContext с моим UnitOfWork? Или как это можно сделать каким-то другим способом, чтобы заставить ASP.NET Identity работать с UnitOfWork?

Я попытался разоблачить DbContext как общедоступное свойство класса UnitOfWork, например:

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

Однако я не думаю, что это правильно - он не работает с пользовательским интерфейсом IDbContext и делает код не очень хорошим для модульного тестирования.

Я также пытался реализовать CustomUserStore и CustomRoleStore - в целом он работал, но, как я его тестировал, он требовал внедрить все больше и больше методов. Это решение выглядит слишком сложно - я действительно надеюсь, что там будет более простой способ.

4b9b3361

Ответ 1

Я нашел работу с ASP.Net Identity 2.0 и EF6 немного сложной задачей. Самый большой недостаток - отсутствие документации или противоречивой документации.

Я использую WebApi 2.0, EF6 и ASP.Net Identity 2.0. Сначала было сложно идти, но как только он работал, это было хорошо.

Я создал свои собственные классы Identity. На данный момент я не забочусь о расширении классов идентичности, я просто хочу сгенерировать таблицы и войти в систему.

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> {}

Пользователь

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, поэтому я изменил имена.

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

    }
}

Я обнаружил, что 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;
    }
}

Я использую шаблон Unit of Work для большинства моих данных. Он работает хорошо. Есть некоторые случаи, когда у меня есть данные, которые требуют большего контроля, чем единица работы, предоставляемая для этих случаев. Я раскрыл 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. Во-первых, я уверен, что Microsoft неплохо справилась с этим. Я уверен, что они сделали лучшую работу, чем вы или я. Если вы действительно хотите протестировать код ASP.NET Identity, поместите его за интерфейс и издевайтесь над интерфейсом.

Ответ 2

Нашел какое-то решение, которое выглядит достаточно общим, но я все еще не уверен, действительно ли он хорош и не нарушает принципы шаблона репозитория /UnitOfWork.

Я добавил общий метод GetDbContext() в свой IUnitOfWork:

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

    ...
}

Как он используется в контроллере, инициализируя 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, так что это может быть не так уж плохо.

Ответ 3

"Одна из проблем, о которых следует помнить, заключается в том, что класс UserStore не очень хорошо работает при использовании шаблона проектирования единицы измерения. В частности, UserStore вызывает SaveChanges почти во всех вызовах по умолчанию по умолчанию, что упрощает преждевременное совершение единица работы. Чтобы изменить это поведение, измените флаг AutoSaveChanges в UserStore."

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

Ответ 4

Если вы используете шаблон Repository и UnitofWork, вы можете использовать его с DDD (Domain Driven Design), где вы объявляете IRepository или IUnitofWork в Core project вместе со всеми другими моделью домена и абстрактными классами.

Теперь вы создаете Проект инфраструктуры, который реализует эти интерфейсы в основном проекте, используя конкретный объект доступа к данным для этого экземпляра Entity Framework. поэтому DbContext в порядке, но да, не выставляйте его на уровень презентации. Поэтому в какой-то момент, если вы хотите изменить EF на любой другой ORM, тогда это будет проще, не касаясь слоя презентации, когда вы ставите свои классы Identity отдельно от проекта доступа к данным или инфраструктуры. И, конечно, вы можете использовать контейнер IOC для создания этих конкретных хранилищ из инфраструктуры в контроллерах уровня представления.