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

Интеграция идентификатора ASP.NET в существующий DbContext

Я работаю над проектом ASP.NET MVC 5 в VS2013,.NET 4.5.1, который использует Entity Framework 6 Code-First. У меня есть база данных приличного размера, построенная и несколько работающая (проект составляет около двух недель). Я хочу интегрировать аутентификацию пользователя сейчас, но я не уверен, как подойти к ней. Проведя большую часть дня исследования, я решил дать новой структуре ASP.NET Identity возможность написать собственные поставщики членства или роли. Я запутался в том, как заставить все работать с существующей базой данных/моделью, которую у меня есть.

В настоящее время у меня есть объект с именем Employee, который содержит основную информацию о сотрудниках (на данный момент). Подумав об этом весь день, я решил отделить аутентификацию от него до объекта User, чего хочет Идентификация. Это сказано, как я могу заставить все это работать?

Здесь мой класс Employee:

public class Employee : Person {
    public int EmployeeId { get; set; }
    public byte CompanyId { get; set; }
    public string Name {
        get {
            return String.Format("{0} {1}", this.FirstName, this.LastName);
        }
    }
    public string Password { get; set; }
    public bool IsActive { get; set; }

    public virtual ICollection<Address> Addresses { get; set; }
    public virtual Company Company { get; set; }
    public virtual ICollection<Email> Emails { get; set; }
    public virtual ICollection<Phone> Phones { get; set; }

    public Employee() {
        this.Addresses = new List<Address>();
        this.Emails = new List<Email>();
        this.Phones = new List<Phone>();
    }
}

И мой класс DbContext:

public class DatabaseContext : DbContext {
    static DatabaseContext() {
        Database.SetInitializer<DatabaseContext>(new DatabaseInitializer());
    }

    public DatabaseContext()
        : base("Name=DatabaseContext") {
        this.Database.Initialize(true);
    }

    public DatabaseContext(
        string connectionString)
        : base(connectionString) {
        this.Database.Initialize(true);
    }

    /// DbSets...

    public override int SaveChanges() {
        try {
            return base.SaveChanges();
        } catch (DbEntityValidationException e) {
            IEnumerable<string> errors = e.EntityValidationErrors.SelectMany(
                x =>
                    x.ValidationErrors).Select(
                x =>
                    String.Format("{0}: {1}", x.PropertyName, x.ErrorMessage));

            throw new DbEntityValidationException(String.Join("; ", errors), e.EntityValidationErrors);
        }
    }

    protected override void OnModelCreating(
        DbModelBuilder modelBuilder) {
        modelBuilder.Ignore<Coordinate>();

        /// Configs...

        base.OnModelCreating(modelBuilder);
    }
}
4b9b3361

Ответ 1

Итак, потратив около дня на чтение или чтение, я закончил создание собственной реализации Identity. Сначала я сделал свой существующий объект Employee и расширил его, чтобы наследовать от IUser<int>. IUser<int> - это интерфейс, который является частью Identity 2.0 (в настоящее время в альфа), который позволяет настроить тип первичного ключа на нечто, отличное от string, как было указано в 1.0. Из-за того, как я храню данные, моя реализация была действительно конкретной. Например, Employee может иметь несколько связанных с ним объектов Email, а для моего приложения я хотел использовать электронные письма в качестве имен пользователей. Итак, я просто установил свойство UserName для возврата рабочего сообщения Employee:

public string UserName {
    get {
        if (this.WorkEmail != null) {
            return this.WorkEmail.Address;
        }

        return null;
    }
    set {
        /// This property is non-settable.
    }
}

Боковое замечание, так как я не собираюсь использовать setter для этого свойства, есть ли более чистый способ его переустановки, кроме простого оставления его пустым?

Перейдя, я также добавил свойство PasswordHash. Я добавил свой собственный объект Role, наследующий от IRole<int>. Наконец, объекты Employee и Role имеют ссылку ICollection<T>, связывающую друг друга. Еще одно замечание: реализация Identity Framework Identity вручную создает таблицу сопоставления UserRoles, а не использует ее собственные возможности настройки, и я не могу понять, что это за ней. Созданный UserRole передается в *Store, который он реализует, но на самом деле он не делает ничего особенного, кроме как действовать как ссылка. В моей реализации я просто использовал уже установленную ссылку, которая, конечно, создает таблицу сопоставления в базе данных, но не бесцельно раскрывается в приложении. Мне просто интересно.

Перейдя снова, с моими настроенными объектами я пошел вперед и реализовал свои собственные классы IUserStore и IRoleStore, творчески называемые EmployeeStore и RoleStore:

public class EmployeeStore : IQueryableUserStore<Employee, int>, IUserStore<Employee, int>, IUserPasswordStore<Employee, int>, IUserRoleStore<Employee, int>, IDisposable {
    private bool Disposed;
    private IDatabaseRepository<Role> RolesRepository { get; set; }
    private IDatabaseRepository<Employee> EmployeesRepository { get; set; }

    public EmployeeStore(
        IDatabaseRepository<Role> rolesRepository,
        IDatabaseRepository<Employee> employeesRepository) {
        this.RolesRepository = rolesRepository;
        this.EmployeesRepository = employeesRepository;
    }

    #region IQueryableUserStore Members
    public IQueryable<Employee> Users {
        get {
            return this.EmployeesRepository.Set;
        }
    }
    #endregion

    #region IUserStore Members
    public async Task CreateAsync(
        Employee employee) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        await this.EmployeesRepository.AddAndCommitAsync(employee);
    }

    public async Task DeleteAsync(
        Employee employee) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        await this.EmployeesRepository.RemoveAndCommitAsync(employee);
    }

    public Task<Employee> FindByIdAsync(
        int employeeId) {
        this.ThrowIfDisposed();

        return Task.FromResult<Employee>(this.EmployeesRepository.FindSingleOrDefault(
            u =>
                (u.Id == employeeId)));
    }

    public Task<Employee> FindByNameAsync(
        string userName) {
        this.ThrowIfDisposed();

        return Task.FromResult<Employee>(this.EmployeesRepository.FindSingleOrDefault(
            e =>
                (e.UserName == userName)));
    }

    public async Task UpdateAsync(
        Employee employee) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        await this.EmployeesRepository.CommitAsync();
    }
    #endregion

    #region IDisposable Members
    public void Dispose() {
        this.Dispose(true);

        GC.SuppressFinalize(this);
    }

    protected void Dispose(
        bool disposing) {
        this.Disposed = true;
    }

    private void ThrowIfDisposed() {
        if (this.Disposed) {
            throw new ObjectDisposedException(base.GetType().Name);
        }
    }
    #endregion

    #region IUserPasswordStore Members
    public Task<string> GetPasswordHashAsync(
        Employee employee) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        return Task.FromResult<string>(employee.PasswordHash);
    }

    public Task<bool> HasPasswordAsync(
        Employee employee) {
        return Task.FromResult<bool>(!String.IsNullOrEmpty(employee.PasswordHash));
    }

    public Task SetPasswordHashAsync(
        Employee employee,
        string passwordHash) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        employee.PasswordHash = passwordHash;

        return Task.FromResult<int>(0);
    }
    #endregion

    #region IUserRoleStore Members
    public Task AddToRoleAsync(
        Employee employee,
        string roleName) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        if (String.IsNullOrEmpty(roleName)) {
            throw new ArgumentNullException("roleName");
        }

        Role role = this.RolesRepository.FindSingleOrDefault(
            r =>
                (r.Name == roleName));

        if (role == null) {
            throw new InvalidOperationException("Role not found");
        }

        employee.Roles.Add(role);

        return Task.FromResult<int>(0);
    }

    public Task<IList<string>> GetRolesAsync(
        Employee employee) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        return Task.FromResult<IList<string>>(employee.Roles.Select(
            r =>
                r.Name).ToList());
    }

    public Task<bool> IsInRoleAsync(
        Employee employee,
        string roleName) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        if (String.IsNullOrEmpty(roleName)) {
            throw new ArgumentNullException("roleName");
        }

        return Task.FromResult<bool>(employee.Roles.Any(
            r =>
                (r.Name == roleName)));
    }

    public Task RemoveFromRoleAsync(
        Employee employee,
        string roleName) {
        this.ThrowIfDisposed();

        if (employee == null) {
            throw new ArgumentNullException("employee");
        }

        if (String.IsNullOrEmpty(roleName)) {
            throw new ArgumentNullException("roleName");
        }

        Role role = this.RolesRepository.FindSingleOrDefault(
            r =>
                (r.Name == roleName));

        if (role == null) {
            throw new InvalidOperationException("Role is null");
        }

        employee.Roles.Remove(role);

        return Task.FromResult<int>(0);
    }
    #endregion
}

RoleStore:

public class RoleStore : IQueryableRoleStore<Role, int>, IRoleStore<Role, int>, IDisposable {
    private bool Disposed;
    private IDatabaseRepository<Role> RolesRepository { get; set; }

    public RoleStore(
        IDatabaseRepository<Role> rolesRepository) {
        this.RolesRepository = rolesRepository;
    }

    #region IQueryableRoleStore Members
    public IQueryable<Role> Roles {
        get {
            return this.RolesRepository.Set;
        }
    }
    #endregion

    #region IRoleStore Members
    public async Task CreateAsync(
        Role role) {
        this.ThrowIfDisposed();

        if (role == null) {
            throw new ArgumentNullException("role");
        }

        await this.RolesRepository.AddAndCommitAsync(role);
    }

    public async Task DeleteAsync(
        Role role) {
        this.ThrowIfDisposed();

        if (role == null) {
            throw new ArgumentNullException("role");
        }

        await this.RolesRepository.RemoveAndCommitAsync(role);
    }

    public Task<Role> FindByIdAsync(
        int roleId) {
        this.ThrowIfDisposed();

        return Task.FromResult<Role>(this.RolesRepository.FindSingleOrDefault(
            r =>
                (r.Id == roleId)));
    }

    public Task<Role> FindByNameAsync(
        string roleName) {
        this.ThrowIfDisposed();

        return Task.FromResult<Role>(this.RolesRepository.FindSingleOrDefault(
            r =>
                (r.Name == roleName)));
    }

    public async Task UpdateAsync(
        Role role) {
        this.ThrowIfDisposed();

        if (role == null) {
            throw new ArgumentNullException("role");
        }

        await this.RolesRepository.CommitAsync();
    }
    #endregion

    #region IDisposable Members
    public void Dispose() {
        this.Dispose(true);

        GC.SuppressFinalize(this);
    }

    protected void Dispose(
        bool disposing) {
        this.Disposed = true;
    }

    private void ThrowIfDisposed() {
        if (this.Disposed) {
            throw new ObjectDisposedException(base.GetType().Name);
        }
    }
    #endregion
}

Теперь я заметил, что реализация Entity Framework создавала то, что выглядело как мини-репозиторий. Поскольку мой проект уже использовал мою собственную репозиториальную реализацию, я решил использовать ее вместо этого. Посмотрим, как это получится...

Теперь все это работает и на удивление не сбой вообще, или, по крайней мере, еще нет. При этом у меня есть все эти замечательные реализации Identity, но я не могу понять, как использовать их в моем приложении MVC. Поскольку это выходит за рамки этого вопроса, я пойду дальше и открою новую, адресованную этому.

Я оставляю это как ответ на вопрос, если кто-то еще столкнется с этим в будущем. Конечно, если кто-нибудь увидит ошибку в коде, который я опубликовал, сообщите мне.

Ответ 2

Взгляните на Исходный код SimpleSecurity Project на примере того, как расширенный контекст базы данных ASP.NET Identity был расширен для включения новых таблиц, Это может сработать для вашей ситуации. Вот как новый контекст был определен путем наследования из контекста Identity ASP.NET.

public class SecurityContext : IdentityDbContext<ApplicationUser>
{
    public SecurityContext()
        : base("SimpleSecurityConnection")
    {
    }


    public DbSet<Resource> Resources { get; set; }
    public DbSet<OperationsToRoles> OperationsToRoles { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Configurations.Add(new ResourceConfiguration());
        modelBuilder.Configurations.Add(new OperationsToRolesConfiguration());
    }
}

SimpleSecurity Project отделяет идентификатор ASP.NET от вашего приложения MVC и расширяет его.

Так как ваш класс Employee является профилем пользователя для членства, я бы посмотрел на его пошив, чтобы он соответствовал как вы настраиваете профиль пользователя в ASP.NET Identity, который обсуждается здесь. В основном ваш класс Employee должен наследовать от IdentityUser, и вы удаляете свойство Password из Employee, так как это определено в IdentityUser, и инфраструктура ищет его там. Затем, определяя свой контекст, вы должны использовать класс Employee, чтобы он выглядел примерно так.

public class DatabaseContext : IdentityDbContext<Employee>
{
  ...
}

Ответ 3

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

Для вашего пользовательского класса:

public class DomainUser : IdentityUser
{
    public DomainUser(string userName) : base(userName) {}

    public DomainUser() {}
}

Для реализации DbContext:

public class DomainModelContext : IdentityDbContext<DomainUser>
{
    public DomainModelContext()
        : base() {}

    public DomainModelContext(string nameOrConnectionString)
        : base(nameOrConnectionString) {}

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

И в Startup.Auth.cs:

    public static Func<UserManager<DomainUser>> UserManagerFactory { get; set; }

    static Startup()
    {
        UserManagerFactory = () => new UserManager<DomainUser>(new UserStore<DomainUser>(new DomainModelContext()));
    }

Другим потенциальным вариантом является создание отношения 1-1 между вашим классом DomainUser и классом ApplicationUser, который наследуется от IdentityUser. Это уменьшило бы связь между вашей моделью домена и механизмом Identity, особенно если вы использовали WithRequiredDependent без создания двунаправленного свойства навигации, примерно так:

modelBuilder.Entity<ApplicationUser>().HasRequired(au => au.DomainUser).WithRequiredPrincipal();