Entity Framework 6を使用したASP.NET MVC 5アプリケーションでリポジトリと作業単位のパターンを学習しています。
私はすでに多くのチュートリアルと記事を読んでいましたが、それらのほとんどすべては矛盾しています。リポジトリと作業単位のパターンは優れていると言う人もいれば、DbContextはすでにリポジトリと作業単位であると言う人もいれば、似たようなことを言うが、まったく異なるアプローチを提供する人もいます。私はこれらすべてのアプローチを試しましたが(すべてではないかもしれません)、どのアプローチが最も正しいかについてまだ苦労しています。
私が現在持っているものは:
コードを貼り付ける必要があるかどうかはわかりませんが、かなり一般的であり、実際にはRepository/UnitOfWork自体には問題がないと思います。私が抱えている問題は、ASP.NET Identityクラスをリポジトリと作業単位と組み合わせて使用することです。メンバーシップと他のすべてのデータで同じデータベースを共有しています。これは一般的なシナリオだと思います。リポジトリを使用してASP.NET Identityクラスをインスタンス化する方法はありません。
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(_DBCONTEXT_);
this.UserManager = new UserManager<ApplicationUser>(store);
[〜#〜] dbcontext [〜#〜]の代わりに何を配置すれば、UnitOfWorkと同じDbContextを共有できますか?または、ASP.NET IdentityをUnitOfWorkと連携させるために他の方法でどのように行うことができますか?
次のようなUnitOfWorkクラスのパブリックプロパティとしてDbContextを公開してみました。
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(this.unitOfWork.MyDbContext);
しかし、私はそれが正しいとは思わない-それはカスタムIDbContextインターフェースで動作せず、コードをユニットテストに適していません。
また、CustomUserStoreとCustomRoleStoreを実装しようとしました-一般的には機能しましたが、テスト中に、より多くのメソッドを実装する必要がありました。この解決策は複雑すぎるように見えます。もっと簡単な方法があるはずです。
ASP.Net Identity 2.0およびEF6での作業は少し難しいと感じました。最大の欠点は、ドキュメントの欠如またはドキュメントの矛盾です。
WebApi 2.0、EF6、およびASP.Net Identity 2.0を使用しています。最初はうまくいくのが大変でしたが、いったん機能し始めてからは良かったです。
独自のIdentityクラスを作成しました。現時点では、テーブルを生成してシステムにログインするだけで、IDクラスの拡張については気にしません。
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; }
}
IDテーブルの名前が気に入らないので、名前を変更しました。
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<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;
}
}
ほとんどのデータアクセスに作業単位パターンを使用しています。うまく機能します。 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<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);
}
}
動作中のいくつかの例を示します。 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 Identityを使用し、その他すべてには作業単位を使用します。
テスト
ASP.NET Identityをテストするつもりはありません。 1つは、Microsoftがテストでかなり良い仕事をしたと確信しています。彼らはあなたや私よりも良い仕事をしたと確信しています。 ASP.NET Identityコードを実際にテストする場合は、インターフェイスの背後に置き、インターフェイスをモックアウトします。
「注意すべき問題の1つは、作業単位のデザインパターンを使用すると、UserStoreクラスが適切に動作しないことです。具体的には、UserStoreはデフォルトでほぼすべてのメソッド呼び出しでSaveChangesを呼び出し、 。この動作を変更するには、UserStoreのAutoSaveChangesフラグを変更します。」
var store = new UserStore<ApplicationUser>(new ApplicationDbContext());
store.AutoSaveChanges = false;
Scott Allenから: http://odetocode.com/blogs/scott/archive/2014/01/03/asp-net-identity-with-the-entity-framework.aspx
ある種の解決策を見つけましたが、これは十分に一般的に見えますが、それが本当に良いもので、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のいくつかの問題を回避するためだけに使用されると思われるので、それほど悪くないかもしれません。
リポジトリとUnitofWorkパターンを使用している場合、他のすべてのドメインモデルと抽象クラスとともにコアプロジェクトでIRepositoryまたはIUnitofWorkを宣言するDDD(ドメインドリブンデザイン)で使用している可能性があります。
ここでInfrastructure projectを作成します。これは、このインスタンスEntity Frameworkの具体的なデータアクセスオブジェクトを使用して、Coreプロジェクトにこれらのインターフェイスを実装します。そのため、DbContextは問題ありませんが、プレゼンテーション層に公開しません。そのため、EFを他のORMに変更したい場合、プレゼンテーションクラスに触れることなく、データアクセスまたはインフラストラクチャプロジェクトとは別にIDクラスを配置する方が簡単です。そしてもちろん、IOC containerを使用して、プレゼンテーション層のコントローラーのインフラストラクチャーからこれらの具体的なリポジトリーをインスタンス化できます。