私は、VS 2013のASP.NET MVC 5プロジェクトに取り組んでいます。NET4.5.1では、Entity Framework 6 Code-Firstを使用しています。私はまともなサイズのデータベースを構築し、ある程度動作しています(プロジェクトは約2週間前です)。ユーザー認証を今すぐ統合したいのですが、どのようにアプローチするのかわかりません。ほとんどの時間を研究に費やした後、新しい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);
}
}
だから、1日かそこらを読んだり読んだりした後、私は自分のIdentity実装を構築することになりました。まず、既存のEmployee
オブジェクトを取得して、IUser<int>
から継承するように拡張しました。 IUser<int>
は、Identity 2.0(現在はアルファ版)の一部であるインターフェイスです。これにより、主キータイプを1.0のデフォルトであるstring
以外に設定できます。データを保存する方法のため、私の実装は本当に具体的でした。たとえば、Employee
には複数のEmail
オブジェクトを関連付けることができ、私のアプリケーションでは、ユーザー名として電子メールを使用したいと考えていました。そのため、UserName
プロパティを設定してEmployee
の仕事用メールを返すだけです。
public string UserName {
get {
if (this.WorkEmail != null) {
return this.WorkEmail.Address;
}
return null;
}
set {
/// This property is non-settable.
}
}
サイドノートは、プロパティにセッターを使用するつもりはないので、単に空のままにする以外に、それを廃止するよりクリーンな方法がありますか?
次に、PasswordHash
プロパティも追加しました。 IRole<int>
を継承して、独自のRole
オブジェクトを追加しました。最後に、Employee
およびRole
オブジェクトにはそれぞれ、相互にリンクするICollection<T>
があります。もう1つの注意点として、IdentityのEntity Framework実装は、独自の構成機能を活用するのではなく、手動でマッピングテーブルUserRoles
を作成します。その背後にある理由を理解できないようです。作成するUserRole
は、実装する*Store
sに渡されますが、実際にはリンクとして機能する以外の特別なことは行いません。私の実装では、すでに確立されたリンクを使用しました。これはもちろんデータベースにマッピングテーブルを作成しますが、アプリケーションに無意味に公開されるわけではありません。好奇心が強いだけです。
もう一度、構成済みのオブジェクトを使用して、創造的に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アプリケーション内でそれらをどのように活用するのかわかりません。これはこの質問の範囲外であるため、先に進み、それに対処する新しい質問を開きます。
将来他の誰かがこれに遭遇した場合の質問への回答としてこれを残しています。もちろん、誰かが私が投稿したコードにエラーを見つけたら、私に知らせてください。
すべての状況に適合するソリューションはありませんが、私のプロジェクトでは、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()));
}
別の潜在的なオプションは、DomainUserクラスとIdentityUserから継承するApplicationUserクラスの間に1-1の関係を作成することです。これにより、特に双方向ナビゲーションプロパティを作成せずにWithRequiredDependentを使用した場合、ドメインモデルとIdentityメカニズム間の結合が減少します。
modelBuilder.Entity<ApplicationUser>().HasRequired(au => au.DomainUser).WithRequiredPrincipal();
ASP.NET Identityのデータベースコンテキストが新しいテーブルを含むように拡張された例については、 SimpleSecurity Project source code をご覧ください。これはあなたの状況でうまくいくかもしれません。 ASP.NET Identityコンテキストから継承することにより、新しいコンテキストがどのように定義されたかを以下に示します。
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はMVCアプリケーションからASP.NET Identityを分離します を拡張します。
Employeeクラスはメンバーシップのユーザープロファイルのように見えるので、それに合わせて調整することを検討します ここで説明するASP.NET Identityでユーザープロファイルをカスタマイズする方法 。基本的に、EmployeeクラスはIdentityUserから継承する必要があります。これは、IdentityUserで定義されており、フレームワークがそこで検索するため、EmployeeからPasswordプロパティを削除します。次に、コンテキストを定義するときに、代わりにEmployeeクラスを使用して、次のようにします。
public class DatabaseContext : IdentityDbContext<Employee>
{
...
}