1

はS/O上の次のような質問がたくさんありますが、これは私が対処見ていない特定の問題があります。リポジトリパターン+依存性の注入+のUnitOfWork + EF

これは、MVCアプリケーションです。 Dependancy injection(Simple Injector、私はそれは無関係だと思うが)を使用しています。それはPer Web Requestを注入します。

私が持っている主な問題は、Webリクエストごとに私のUoWが注入されるため、私が最近必要だったデータを追加するときに失敗し続けてしまうことです。

次のコードは示しています

データ層

public abstract RepositoryBase<TEntity> 
{ 
    private readonly MyDbContext _context; 

    //fields set from contrstuctor injection 
    protected RepositoryBase(MyDbContext context) 
    { 
     _context = context; 
    } 

    public IList<TEntity> GetAll() 
    { 
     return _context.Set<TEntity>().ToList(); 
    } 

    public TEntity GetById(Int32 id) 
    { 
     _context.Set<TEntity>().Find(id); 
    } 

    public TEntity Insert(TEntity entity) 
    { 
     _context.Set<TEntity>().Add(entity); 
    } 
} 

public UserRepository : RepositoryBase<User>, IUserRepository 
{ 
    //constructor injection 
    public UserRepository(MyDbContext c) : base(c) {} 

    public Update(Int32 id, String name, String email, Int32 ageYears) 
    { 
     var entity = GetById(id); 
     entity.Name = name; 
     entity.Email = email; 
     entity.Age = ageYears; 
    } 

    public UpdateName(Int32 id, String name) 
    { 
     var entity = GetById(id); 
     entity.Name = name; 
    } 
} 

public AddressRepository : RepositoryBase<Address>, IAddressRepository 
{ 
    //constructor injection 
    public AddressRepository(MyDbContext c) : base(c) {} 

    public Update(Int32 id, String street, String city) 
    { 
     var entity = GetById(id); 
     entity.Street = street; 
     entity.City = city; 
    } 

    public Address GetForUser(Int32 userId) 
    { 
     return _context.Adresses.FirstOrDefault(x => x.UserId = userId); 
    } 
} 

public DocumentRepository : RepositoryBase<Document>, IDocumentRepository 
{ 
    //constructor injection 
    public DocumentRepository(MyDbContext c) : base(c) {} 

    public Update(Int32 id, String newTitle, String newContent) 
    { 
     var entity.GetById(id); 
     entity.Title = newTitle; 
     entity.Content = newContent; 
    } 

    public IList<Document> GetForUser(Int32 userId) 
    { 
     return _context.Documents.Where(x => x.UserId == userId).ToList(); 
    } 
} 

public UnitOfWork : IUnitOfWork 
{ 
    private readonly MyDbContext _context; 

    //fields set from contrstuctor injection 
    public UnitOfWork(MyDbContext context) 
    { 
     _context = context; 
    } 

    public Int32 Save() 
    { 
     return _context.SaveChanges(); 
    } 

    public ITransaction StartTransaction() 
    { 
     return new Transaction(_context.Database.BeginTransaction(IsolationLevel.ReadUncommitted)); 
    } 
} 

public Transaction : ITransaction 
{ 
    private readonly DbContextTransaction _transaction; 

    public Transaction(DbContextTransaction t) 
    { 
     _transaction = t; 
     State = TransactionState.Open; 
    } 

    public void Dispose() 
    { 
     if (_transaction != null) 
     { 
      if (State == TransactionState.Open) 
      { 
       Rollback(); 
      } 
      _transaction.Dispose(); 
     } 
    } 

    public TransactionState State { get; private set; } 

    public void Commit() 
    { 
     try 
     { 
      _transaction.Commit(); 
      State = TransactionState.Committed; 
     } 
     catch (Exception) 
     { 
      State = TransactionState.FailedCommitRolledback; 
      throw; 
     } 
    } 

    public void Rollback() 
    { 
     if (_transaction.UnderlyingTransaction.Connection != null) 
     { 
      _transaction.Rollback(); 
     } 
     State = TransactionState.Rolledback; 
    } 
} 

サービス層

public DocumentService : IDocumentService 
{ 
    //fields set from contrstuctor injection 
    private readonly IDocumentRepository _docRepo; 
    private readonly IUnitOfWork _unitOfWork; 

    public void AuthorNameChangeAddendum(Int32 userId, String newAuthorName) 
    { 
     //this works ok if error thrown 
     foreach(var doc in _docRepo.GetForUser(userId)) 
     { 
      var addendum = $"\nAddendum: As of {DateTime.Now} the author will be known as {newAuthorName}."; 
      _docRepo.Update(documentId, doc.Title + "-Processed", doc.Content + addendum); 
     } 
     _unitOfWork.Save(); 
    } 
} 

public UserService 
{ 
    //fields set from contrstuctor injection 
    private readonly IUserRepository _userRepo; 
    private readonly IAddressRepository _addressRepo; 
    private readonly IUnitOfWork _unitOfWork; 
    private readonly IDocumentService _documentService; 

    public void ChangeUser(Int32 userId, String newName, String newStreet, String newCity) 
    { 
     //this works ok if error thrown 
     _userRepo.UpdateName(userId, newName); 

     var address = _addressRepo.GetForUser(userId); 
     _addressRepo.Update(address.AddressId, newStreet, newCity); 

     _unitOfWork.Save(); 
    } 

    public void ChangeUserAndProcessDocs(Int32 userId, String newName, Int32) 
    { 
     //this is ok because of transaction 
     using(var transaction = _unitOfWork.StartTransaction()) 
     { 
      _documentService.AuthorNameChangeAddendum(userId, newName); //this function calls save() on uow 

      //possible exception here could leave docs with an inaccurate addendum, so transaction needed 
      var x = 1/0; 

      _userRepo.UpdateName(userId, newName); 

      _unitOfWork.Save(); 
      transaction.Commit(); 
     } 
    } 

    //THE PROBLEM: 
    public IList<String> AddLastNameToAll(String lastName) 
    { 
     var results = new List<String>(); 
     foreach(var u in _userRepo.GetAll()) 
     { 
      try 
      { 
       var newName = $"{lastName}, {u.Name}"; 
       _userRepo.UpdateName(u.UserId, newName); 
       _unitOfWork.Save(); //throws validation exception 
       results.Add($"Changed name from {u.Name} to {newName}."); 
      } 
      catch(DbValidationException e) 
      { 
       results.Add($"Error adding last name to {u.Name}: {e.Message}"); 
       //but all subsequeqnet name changes will fail because the invalid entity will be stuck in the context 
      } 
     } 
     return results; 
    } 
} 

あなたは、UOWの実装がChangeUser()を処理するUserServiceの中で見ることができますし、 ChangeUserAndProcessDocs()の潜在的な問題明示的なトランザクションの使用によって処理されます。

しかし、AddLastNameToAll()では、名前の列が新しい名前を処理するのに十分な長さではないため、100人のユーザーが更新され、3番目のユーザーが失敗した場合、結果3〜100のすべてに同じ検証メッセージが表示されますそれら。これを解決する唯一の方法は、forループの各パスに対して新しいUnitOf Work(DbContext)を使用することです。これは私の実装では実際には不可能です。

私のUoW + Repo実装は、EFが他のレイヤーに漏れることを防ぎ、他のレイヤーにトランザクションを作成する機能を提供します。しかし、AサービスがBサービスを呼び出すと、BサービスはAが準備される前にSave()を呼び出すことができるということはいつも不思議に思っています。スコープされたトランザクションはそれを解決しますが、まだ少し奇妙に感じます。

私はUoWパターンの廃棄を考えていましたが、すべてのリポジトリアクションを即座にコミットするだけですが、2つの異なるエンティティタイプを更新して2回目の更新を失敗させるという大きな問題は残しましたが、今(ChangeUserAndProcessDocs()は一例で参照してください。

だから私は、注入された文脈を無視し、それが自分だ作成UserRepository UpdateNameImmediately()の特別UpdateName()機能させることが残っています。

public void UpdateNameImmediately(Int32 id, String newName) 
    { 
     using(var mySingleUseContext = new MyDbContext()) 
     { 
      var u = mySingleUseContext.Users.Find(id); 
      u.Name = newName; 
      mySingleUseContext.SaveChanges(); 
     } 
    } 

これは奇妙に感じ、今、このため、関数はq私の他のすべてのリポジトリのアクションと違って、トランザクションに従わないということです。

これを解決するUoW + EF +リポジトリパターン+ DIの実装はありますか?どのように動作する

+1

EFはデフォルトでUoWとRepoパターンを実装していますか? DbContextはUoWで、DbSetはReposです。私のポイント; EFにさらに抽象化を加えることは時間の無駄です。 –

+0

私はDbContextがUoWであることを知りました。私はEFをサービスレイヤーに漏らさないために、その上に薄いラッパーしかありません。その周りのレポ・ラッパーは、いくつかのリポジトリ・アクションを通してすべての私を援助し、悪い結合などを警察するのに役立ちます。これらのことを達成するためのよりよい方法があれば、私はそれを聞いています。 – sheamus

+0

AddLastNameToAll()に示された問題は、実際にはほとんどすべての一括処理を行うきわめて特化したサービスで発生します。私はUoWパターンを使用しない特殊なImmediateRepositoriesを作成することに傾いています。私はこれが最も簡単だと思います。 – sheamus

答えて

0

AddLastNameToAll()のソリューションは、即座に各変更をコミットするソリューションは「即時リポジトリラッパー」でした。これにより私は既存のコードを再利用することができ、私のサービスをテストする際にリポジトリの動作を簡単に模倣することができました。ユーザーのための例の即時リポジトリラッパーは次のようになります。これは私がすぐにコミットを必要とする大量処理のまれなシナリオのために非常によく働いている

public interface IUserImmediateRepository 
{ 
    void UpdateName(Int32 id, String newName); 
} 

public class UserImmediateRepository : IUserImmediateRepository 
{ 
    public UserImmediateRepository() 
    { 
    } 

    public void UpdateName(Int32 id, String newName) 
    { 
     using(var singleUseContext = new MyDbContext()) 
     { 
      var repo = new UserRepository(singleUseContext); 
      repo.UpdateName(id, newName); 
      singleUseContext.SaveChanges(); 
     } 
    } 
} 

1

public class DbFactory : Disposable, IDbFactory 
    { 
     HomeCinemaContext dbContext; 



public HomeCinemaContext Init() 
    { 
     return dbContext ?? (dbContext = new HomeCinemaContext()); 
    } 

    protected override void DisposeCore() 
    { 
     if (dbContext != null) 
      dbContext.Dispose(); 
    } 
} 

public class UnitOfWork : IUnitOfWork 
    { 
     private readonly IDbFactory dbFactory; 
     private HomeCinemaContext dbContext; 

     public UnitOfWork(IDbFactory dbFactory) 
     { 
      this.dbFactory = dbFactory; 
     } 

     public HomeCinemaContext DbContext 
     { 
      get { return dbContext ?? (dbContext = dbFactory.Init()); } 
     } 

     public void Commit() 
     { 
      DbContext.Commit(); 
     } 
    } 

public class EntityBaseRepository<T> : IEntityBaseRepository<T> 
      where T : class, IEntityBase, new() 
    { 

     private HomeCinemaContext dataContext; 

     #region Properties 
     protected IDbFactory DbFactory 
     { 
      get; 
      private set; 
     } 

     protected HomeCinemaContext DbContext 
     { 
      get { return dataContext ?? (dataContext = DbFactory.Init()); } 
     } 
     public EntityBaseRepository(IDbFactory dbFactory) 
     { 
      DbFactory = dbFactory; 
     } 
     #endregion 
     public virtual IQueryable<T> GetAll() 
     { 
      return DbContext.Set<T>(); 
     } 
     public virtual IQueryable<T> All 
     { 
      get 
      { 
       return GetAll(); 
      } 
     } 
     public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) 
     { 
      IQueryable<T> query = DbContext.Set<T>(); 
      foreach (var includeProperty in includeProperties) 
      { 
       query = query.Include(includeProperty); 
      } 
      return query; 
     } 
     public T GetSingle(int id) 
     { 
      return GetAll().FirstOrDefault(x => x.ID == id); 
     } 
     public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate) 
     { 
      return DbContext.Set<T>().Where(predicate); 
     } 

     public virtual void Add(T entity) 
     { 
      DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); 
      DbContext.Set<T>().Add(entity); 
     } 
     public virtual void Edit(T entity) 
     { 
      DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); 
      dbEntityEntry.State = EntityState.Modified; 
     } 
     public virtual void Delete(T entity) 
     { 
      DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); 
      dbEntityEntry.State = EntityState.Deleted; 
     } 
    } 

メインクラスがDbFactoryあり、それはEFのデシベルコンテキストのインスタンスが1つだけ含まれています。ですから、あなたは差分リポジトリで何をするのですか?アプリケーションは常に一つのコンテキストを使います。

クラスEntityBaseRepositoryも同じdbコンテキストで提供されるで動作します.DbFactoryによって提供されます。

UnitOfWorkは、dbコンテキストの同じインスタンスをコミットするメソッドCommitを使用して、データベースへのすべての変更を保存できるようにするためにのみ、コントローラに渡されます。

あなたのコードで使用するトランザクションは必要ありません。

完全チュートリアルはここにある: "DbFactory" または "UnitOfWorkの" 詳細に入るために:

\

https://chsakell.com/2015/08/23/building-single-page-applications-using-web-api-and-angularjs-free-e-book/#architectureは言葉を検索します。

+0

**作業単位あたりのDBコンテキスト** _確かに必要アプリ全体のコンテキスト接続が切断されるとどうなりますか? – MickyD

+0

アプリケーションごとではなく、ユーザー/リクエストごと。 IOCコンテナで行う必要があります。 –

+1

これは、 "DbFactoryで言うことではありませんが、EF DBコンテキストの1つのインスタンスのみを含んでいます...アプリケーションは常に1つのコンテキスト" _を使用し、注入された 'IDbFactory'は常に' DbFactory .Init'。あなたは、工場の寿命がどのように管理されているかを示す必要がありますし、おそらくそれを_factory_と呼ぶことはありません。 https://www.youtube.com/watch?v=rtXpYpZdOzM – MickyD