web-dev-qa-db-ja.com

作業単位+リポジトリ+依存性注入を伴うサービス層

WebアプリケーションとWindowsサービスを設計していて、作業単位+リポジトリレイヤーをサービスレイヤーと組み合わせて使用​​したいのですが、クライアントアプリがデータのトランザクションを制御できるようにすべてをまとめるのに問題があります。作業単位。

作業単位には、コミットおよびロールバック操作とともに、トランザクションに登録されているすべてのリポジトリーのコレクションがあります。

public interface IUnitOfWork : IDisposable
{
    IRepository<T> Repository<T>() where T : class;
    void Commit();
    void Rollback();
}

汎用リポジトリには、特定のモデル(テーブル)のデータレイヤーで実行される操作があります

public interface IRepository<T> where T : class 
{
    IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, IList<Expression<Func<T, object>>> includedProperties = null, IList<ISortCriteria<T>> sortCriterias = null);
    PaginatedList<T> GetPaged(Expression<Func<T, bool>> filter = null, IList<Expression<Func<T, object>>> includedProperties = null, PagingOptions<T> pagingOptions = null);
    T Find(Expression<Func<T, bool>> filter, IList<Expression<Func<T, object>>> includedProperties = null);
    void Add(T t);
    void Remove(T t);
    void Remove(Expression<Func<T, bool>> filter);
}

作業単位の具体的な実装では、内部のエンティティフレームワーク(DbContext)を使用してデータベースへの変更を保存し、作業単位ごとにDbContextクラスの新しいインスタンスを作成します。

public class UnitOfWork : IUnitOfWork
{
    private IDictionary<Type, object> _repositories;
    private DataContext _dbContext;
    private bool _disposed;

    public UnitOfWork()
    {
        _repositories = new Dictionary<Type, object>();
        _dbContext = new DataContext();
        _disposed = false;
    }

作業ユニット内のリポジトリは、現在の作業ユニットインスタンスに存在しない場合、アクセス時に作成されます。リポジトリはDbContextをコンストラクターパラメーターとして受け取るため、現在の作業単位で効果的に機能できます。

public class Repository<T> : IRepository<T> where T : class
{
    private readonly DataContext _dbContext;
    private readonly DbSet<T> _dbSet;

    #region Ctor
    public Repository(DataContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = _dbContext.Set<T>();
    }
    #endregion

また、ビジネスワークフローロジックをカプセル化し、コンストラクターでそれらの依存関係を取得するサービスクラスもあります。

public class PortfolioRequestService : IPortfolioRequestService
{
    private IUnitOfWork _unitOfWork;
    private IPortfolioRequestFileParser _fileParser;
    private IConfigurationService _configurationService;
    private IDocumentStorageService _documentStorageService;

    #region Private Constants
    private const string PORTFOLIO_REQUEST_VALID_FILE_TYPES = "PortfolioRequestValidFileTypes";
    #endregion

    #region Ctors
    public PortfolioRequestService(IUnitOfWork unitOfWork, IPortfolioRequestFileParser fileParser, IConfigurationService configurationService, IDocumentStorageService documentStorageService)
    {
        if (unitOfWork == null)
        {
            throw new ArgumentNullException("unitOfWork");
        }

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

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

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

        _unitOfWork = unitOfWork;
        _fileParser = fileParser;
        _configurationService = configurationService;
        _documentStorageService = documentStorageService;
    }
    #endregion

WebアプリケーションはASP.NET MVCアプリであり、コントローラーはその依存関係をコンストラクターにも挿入します。この場合、作業単位とサービスクラスが注入されます。このアクションは、リポジトリにレコードを作成し、DocumentStorageServiceを使用してファイルをファイルサーバーに保存するなど、サービスによって公開される操作を実行します。その後、作業単位がコントローラーアクションでコミットされます。

public class PortfolioRequestCollectionController : BaseController
{
    IUnitOfWork _unitOfWork;
    IPortfolioRequestService _portfolioRequestService;
    IUserService _userService;

    #region Ctors
    public PortfolioRequestCollectionController(IUnitOfWork unitOfWork, IPortfolioRequestService portfolioRequestService, IUserService userService)
    {
        _unitOfWork = unitOfWork;
        _portfolioRequestService = portfolioRequestService;
        _userService = userService;
    }
    #endregion
[HttpPost]
    [ValidateAntiForgeryToken]
    [HasPermissionAttribute(PermissionId.ManagePortfolioRequest)]
    public ActionResult Create(CreateViewModel viewModel)
    {
        if (ModelState.IsValid)
        {
            // validate file exists
            if (viewModel.File != null && viewModel.File.ContentLength > 0)
            {
                // TODO: ggomez - also add to CreatePortfolioRequestCollection method
                // see if file upload input control can be restricted to Excel and csv
                // add additional info below control
                if (_portfolioRequestService.ValidatePortfolioRequestFileType(viewModel.File.FileName))
                {
                    try
                    {
                        // create new PortfolioRequestCollection instance
                        _portfolioRequestService.CreatePortfolioRequestCollection(viewModel.File.FileName, viewModel.File.InputStream, viewModel.ReasonId, PortfolioRequestCollectionSourceId.InternalWebsiteUpload, viewModel.ReviewAllRequestsBeforeRelease, _userService.GetUserName());
                        _unitOfWork.Commit();                            
                    }
                    catch (Exception ex)
                    {
                        ModelState.AddModelError(string.Empty, ex.Message);
                        return View(viewModel);
                    }

                    return RedirectToAction("Index", null, null, "The portfolio construction request was successfully submitted!", null);
                }
                else
                {
                    ModelState.AddModelError("File", "Only Excel and CSV formats are allowed");
                }
            }
            else
            {
                ModelState.AddModelError("File", "A file with portfolio construction requests is required");
            }
        }


        IEnumerable<PortfolioRequestCollectionReason> portfolioRequestCollectionReasons = _unitOfWork.Repository<PortfolioRequestCollectionReason>().Get();
        viewModel.Init(portfolioRequestCollectionReasons);
        return View(viewModel);
    }

Webアプリケーションでは、Unity DIコンテナーを使用して、httpリクエストごとに同じ作業単位のインスタンスをすべての呼び出し元に挿入しているため、コントローラークラスは新しいインスタンスを取得し、次に作業単位を使用するサービスクラスは同じインスタンスを取得しますコントローラーとして。このようにして、サービスはいくつかのレコードをリポジトリーに追加します。リポジトリーは作業単位に登録されており、コントローラーのクライアント・コードによってコミットできます。

上記のコードとアーキテクチャに関する1つの質問。サービスクラスで作業ユニットの依存関係を取り除くにはどうすればよいですか?理想的には、サービスにトランザクションをコミットさせたくないので、サービスクラスに作業単位のインスタンスを持たせたくありません。サービスに、処理する必要のあるリポジトリへの参照を持たせたいだけです。そして、コントローラー(クライアントコード)が適切と判断したときに操作をコミットします。

Windowsサービスアプリケーションについて、単一の作業単位でレコードのセットを取得できるようにしたいと考えています。次に、これらのすべてのレコードをループしてデータベースにクエリを実行し、各レコードを個別に取得してから、ループごとにそれぞれのステータスを確認します。ステータスは、すべてのクエリを実行した時間から操作したい時間まで変化した可能性があるためです。単一のもの。私が今抱えている問題は、現在のアーキテクチャでは、サービスの同じインスタンスに対して複数の作業単位を使用できないことです。

public class ProcessPortfolioRequestsJob : JobBase
{
    IPortfolioRequestService _portfolioRequestService;
    public ProcessPortfolioRequestsJob(IPortfolioRequestService portfolioRequestService)
    {
        _portfolioRequestService = portfolioRequestService;
    }

上記のJobクラスは、コンストラクター内のサービスを依存関係として受け取り、Unityによって再び解決されます。解決および注入されるサービスインスタンスは、作業ユニットによって異なります。サービスクラスで2つのget操作を実行したいのですが、同じ作業単位のインスタンスで操作しているため、それを実現できません。

そこにいるすべての教祖のために、上記の目標を達成するために、アプリケーション、作業単位+リポジトリ+サービスクラスを再構築する方法について何か提案はありますか?

作業単位とリポジトリパターンを使用してサービスクラスのテストを可能にするつもりでしたが、関心の分離を維持しながら、コードの保守性とテスト性を同時に実現する他のデザインパターンを受け入れています。

pdate 1 EF DbSetと構成を宣言したEFのDbContextから継承するDataContextクラスを追加します。

public class DataContext : DbContext
{
    public DataContext()
        : base("name=ArchSample")
    {
        Database.SetInitializer<DataContext>(new MigrateDatabaseToLatestVersion<DataContext, Configuration>());
        base.Configuration.ProxyCreationEnabled = false;
    }

    public DbSet<PortfolioRequestCollection> PortfolioRequestCollections { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Configurations.Add(new PortfolioRequestCollectionConfiguration());

        base.OnModelCreating(modelBuilder);
    }
}
13
Guillermo Gomez

Unit of Work(UoW)を使用する目的がテスト容易性である場合、間違った道をたどりました。作業単位は、テスト容易性については何もしません。その主な目的は、異なるデータソースにアトミックトランザクションを提供すること、UoW機能をまだ提供していないデータレイヤーに提供すること、または既存のUoWをより簡単に交換できるようにラップすることです...汎用リポジトリを使用して無効にしました(これにより、とにかくEntity Frameworkに緊密に結合されます)。

作業単位を完全に取り除くことをお勧めします。 EntityFrameworkはすでにUoWです。マイクロソフトでさえ彼らの考えを変え、EFでUoWをもはや推奨しません。

したがって、UoWを削除した場合は、リポジトリを使用してEFクエリをラップできます。汎用リポジトリを使用することはお勧めしません。これにより、コード全体(UoWがすでに実行しているもの)にデータレイヤーの実装がリークするため、代わりに具象repoTsitoriesを作成します(必要に応じて、内部で汎用リポジトリを使用できますが、汎用リポジトリリポジトリの外に漏出してはいけません)。

これは、サービスレイヤーが必要な特定の具象リポジトリを取得することを意味します。たとえば、IPortfolioRepository。次に、IPorfolioRepositoryから継承するPortfolioRepositoryクラスがあります。このクラスは、依存性インジェクション(DI)フレームワークによって注入されるパラメーターとしてEF DbContextを取ります。 「PerRequest」ベースでEFコンテキストをインスタンス化するようにDIコンテナーを構成すると、同じインスタンスをすべてのリポジトリーに渡すことができます。リポジトリにCommitを呼び出すSavesChangesメソッドを設定できますが、そのリポジトリだけでなく、すべての変更に対する変更が保存されます。

テスト容易性に関する限り、2つの選択肢があります。具象リポジトリをモックするか、EF6の組み込みのモック機能を使用することができます。

18

私は自分でその地獄の穴を通り抜けてきました、そしてここに何があります 私はしました

  • UoWを完全に捨てます。 EFのDBContext基本的にUoWです。車輪の再発明には意味がありません。

    あたり [〜#〜] msdn [〜#〜]

    DbContextクラス

    Unit-Of-WorkパターンとRepositoryパターンの組み合わせを表し、データベースにクエリを実行して変更をグループ化し、変更を1つの単位としてストアに書き戻すことができます。

  • サービス層+リポジトリ層は良い選択のように思えました。ただし、リポジトリは常にリークのある抽象化であり、特にDBContextのDbSetがリポジトリと同等である場合はそうです。

  • その後、Windowsサービスの必要性が発生すると、別のレイヤーで事態がさら​​に混乱します。非同期またはバックグラウンド処理を組み合わせて投げると、物事はすぐに漏れ始めます。

私の2セントを尋ねると、サービスレイヤー+ EFを使用し、1つはビジネスロジックをラップし、もう1つはUOW /リポジトリパターンをラップします。

あるいは、特にWindowsサービスの場合、 コマンドクエリベース アプローチに移行した方がうまくいくことがわかりました。これはテスト容易性に役立つだけでなく、要求が終了した後もDBContextを存続させることを心配する必要がない非同期タスクにも役立ちます(DBContextはコマンドハンドラーに関連付けられ、非同期コマンドが存続している限り存続します)生きている)。

さて、最近UOW/Repositoryパターンに関するすべての事実を要約してしまった場合は、確かに、コマンドクエリパターンについて読むだけでも心に害を与えるでしょう。私はその道を進んできましたが、私を信じてください。少なくともそれを調べて試してみる価値はあります。

これらの投稿は役立つかもしれません:

(CQRSを介してダイジェストした後)十分に勇気がある場合は、 MediatR を見てください。これはMediatorパターン(基本的にコマンドクエリを通知でラップします)を実装し、pub-subを介して作業できるようにします。 pub-subモデルは、Windowsサービスおよびサービスレイヤーに適しています。

4
Mrchief