DapperとCQRS


導入


2016年に、私が働く会社は、我々の主なソフトウェアスイートの大きな書き直しを開始しました.私たちは教育会社ですので、ソフトウェアは、学校管理や教師が等級本、出席、カリキュラム、評価、行動、スケジュールから必要なすべて含まれています.それは本当に大きな成功だった.教師と行政職員はソフトウェアを楽しんで、大きな一歩としてそれを見ているようでした.我々はみんなとても誇りに思っていた.時には、輝きは消耗した.我々が得るために始めた一般的な苦情は、ユーザーインターフェイスは、バギーとハード移動し、システムが遅くなった混乱していた.
我々が低速度を調査し始めたので、我々はいろいろなAPIへのデータ呼び出しが全く遅いと気がつきました.2018年後半に、我々はAPIへのデータ呼び出しをスピードアップする方法を調べ始めました.これはまさにその頃でした.NETコア2.0が出てきたが、我々は現在使用していた.ネット4.5.3 .それで、現在既存のAPIを再処理する代わりに、我々は完全に中で書かれる新しいAPI層で、ゼロから始めることに決めました.ネットコア.
知ってる.知ってる.我々は、ゼロから始まるこの決定の正しさまたは不正を議論することができます.私たちのすべての時間を節約するには、ちょうど私たちが新しいスタックでやってきたものに移動できます.
それが知られるようになったように、「遺産スタック」は書かれました.Entity Frameworkを使用したNET 4.5.3および古典的なリポジトリ/ワークパターンの単位.Entity Frameworkコアで動作します.ネットコアでは、クエリ速度を改善していないことがわかった.彼らはまだ遅かった.通常、クエリに基づいて250 msの領域で.Entity Frameworkは、この「新しいスタック」コードのためにそれを切るつもりはありませんでした.我々はDapperを試みることにした.Dapperと共に、我々のバックエンドコードがどのように構造化され、データが配送されるかについて、異なるパターンを採用することを決めた.CQRS(コマンドの問い合わせの責任の分離)についてかなり読んだ後、いくつかの素晴らしい例をオンラインで我々はこのパターンに定住見つける.特に、This articleは非常に有用でした.我々は正確に従っていなかったが、我々はそれからかなりの数のアイデアを盗んだ.
この記事の残りの部分は、我々がAPIコードをどのように設定するか、そして、我々が続くパターンの例を通して歩くことです.

コード


この例のすべてのソースコードをhttps://github.com/MelodicDevelopment/example-dotnet-api-cqrsで取得できます.我々は別の時間のコードの組織を議論することができます.私は例をかなり迅速に取得しようとしていた.しかし、これは私たちが私のチームに続くものにかなり近いです.
APIプロジェクトを実行すると、この例のために作成した単純なAPIをドキュメント化するswaggerページになります.

契約


契約プロジェクトには、ソリューションのすべてのインターフェイスとDTOSが含まれます.dtosの例は著者と本です.CQRSパターンに従うことを決めたので、我々のデータベース質問とコマンドは別々のクラスに分けられます.クエリは、IQueryインターフェイスに基づいています.

DOTNET API CQRS。契約/データ/ iQuery。cs


using System.Data;

namespace dotnet_api_cqrs.contracts.data
{
    public interface IQuery<T>
    {
        string Sql { get; set; }

        T Execute(IDbContext context, IDbTransaction transaction = null);
    }
}
コマンドはICommandインタフェースに基づいています.ICommandはすべてのコマンドが整数を返すので、intの一般的なタイプでのIQueryインターフェースの単純な拡張です.

DOTNET API CQRS。契約/データ/ iCommand。cs


namespace dotnet_api_cqrs.contracts.data
{
    public interface ICommand : IQuery<int> { }
}
IDBContextは問い合わせやコマンドを実行するデータベース接続クラスです.実装はデータプロジェクトにありますので、後で詳しく説明します.

DOTNET API CQRS。契約/データ/ idbcontext。cs


using System;
using System.Collections.Generic;
using System.Data;

namespace dotnet_api_cqrs.contracts.data
{
    public interface IDbContext : IDisposable
    {
        T QueryFirst<T>(string query, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null);
        IEnumerable<T> Query<T>(string query, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null);
        int InsertSingle(string sql, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null, int? timeout = null);
        int Command(string sql, object param = null, CommandType commandType = CommandType.Text, IDbTransaction transaction = null, int? timeout = null);
        T Transaction<T>(Func<IDbTransaction, T> query);
        void Transaction(Action<IDbTransaction> query);
    }
}
あなたは、同様に名前ファサードを含む若干のインターフェースに気がつきます.各Facade intefaceは、funcまたはactionデリゲートを返すメソッドを定義します.我々がデータプロジェクトについて話すとき、これらのより多く.

データ:コマンドと質問


データプロジェクトは、あなたが我々のデータアクセス層のまわりの組織の大部分を見るつもりです.最初の2つのフォルダは、コマンドとクエリのためであり、各フォルダの内部には、各DTOのサブフォルダです.DTOフォルダの内部では、問い合わせクラスが表示されます.それぞれのクエリは、それが何のために命名されます.例えば、
  • getallbooksquery
  • GetBookQuery
  • これらのクエリが何をするかを決定するのは難しいことではありません.getallbooksqueryを見てみましょう.

    DOTNET API CQRS。データ/クエリ/ブック/ getallbooksquery。cs


    using System.Collections.Generic;
    using System.Data;
    using dotnet_api_cqrs.contracts.data;
    using D = dotnet_api_cqrs.contracts.dto;
    
    namespace dotnet_api_cqrs.data.Queries.Book
    {
        public class GetAllBooksQuery : IQuery<IEnumerable<D.Book>>
        {
            public string Sql { get; set; }
    
            public GetAllBooksQuery()
            {
                Sql = @$"
    SELECT              BookID,
                        Title,
                        CopyRightYear,
                        AuthorID
    FROM                dbo.Books
    ORDER BY            Title";
            }
    
            public IEnumerable<D.Book> Execute(IDbContext context, IDbTransaction transaction = null)
            {
                return context.Query<D.Book>(Sql, transaction: transaction);
            }
        }
    }
    
    GetAllbookSqueryクラスは、Iquery>インタフェースを実装します.実行したい場合は、IDBContextをExecuteメソッドに渡します.かなり簡単です.ここで、InsertBookCommandクラスを見てみましょう.

    DOTNET API CQRS。データ/コマンド/予約/挿入コマンドブック。cs


    using System.Data;
    using dotnet_api_cqrs.contracts.data;
    using D = dotnet_api_cqrs.contracts.dto;
    
    namespace dotnet_api_cqrs.data.Commands.Book
    {
        public class InsertBookCommand : ICommand
        {
            private readonly D.Book _book;
    
            public string Sql { get; set; }
    
            public InsertBookCommand(D.Book book)
            {
                _book = book;
    
                Sql = $@"
    INSERT INTO         dbo.Books
                        (Title, CopyRightYear, AuthorID)
    VALUES              (@Title, @CopyRightYear, AuthorID);";
            }
    
            public int Execute(IDbContext context, IDbTransaction transaction = null)
            {
                var param = new {
                    Title = _book.Title,
                    CopyRightYear = _book.CopyRightYear,
                    AuthorID = _book.AuthorID
                };
    
                return context.InsertSingle(Sql, param, transaction: transaction);
            }
        }
    }
    
    このクラスは、ブックパラメータを受け取り、SQLパラメータを設定し、IDBContextで実行します.InsertSingleメソッドは、実際に新しい本を返し、新しいidを返します.

    データ:ファサード


    元々、このパターンで開発を開始したとき、我々は、注射のためにそれらをセットアップすることなく、我々のサービスで直接質問とコマンドを使い始めました.我々は、単にサービスのコードでnew GetAllBooksQuery()を右クリックしました.これはテストするのが難しいとわかった.そこで、この問題を解決するためにファサードを使用することを決めた.ファサードは単にDTsのDTOまたは論理グループのために関連した質問とコマンドをグループ化します.もう一つの課題は、サービスとファサードの間で解決したいということでした.そこで、我々は常にファサードメソッドから関数デリゲートを返すパターンを採用しました.これを簡単にするために、子供クラスで使用するいくつかのメソッドを使用してファサードベースクラスを作成しました.クエリまたはコマンドから値を返す場合は、funcデリゲートを使用します.問い合わせまたはコマンドから戻り値を必要としない場合は、アクションデリゲートを使用します.各関数デリゲートは、IDBContextと可能なIDBTransactionを受け取ります.これにより、クエリやコマンドを実行するために異なるコンテキストで渡すことができます.Transactionで渡すと、同じトランザクションで複数のデータベースアクションを実行できます.DBContextは、トランザクション内のすべてのクエリとコマンドを実行する設定です.DBContextのトランザクションメソッドを使用して、複数のデータベースアクションを実行する独自のトランザクションを作成することもできます.

    DOTNET API CQRS。データ/ファサード.cs


    using System;
    using System.Data;
    using dotnet_api_cqrs.contracts.data;
    
    namespace dotnet_api_cqrs.data
    {
        public class Facade
        {
            protected Func<IDbContext, IDbTransaction, T> Prepare<T>(IQuery<T> queryCommand)
            {
                return (context, transaction) => {
                    return queryCommand.Execute(context, transaction);
                };
            }
    
            protected Action<IDbContext, IDbTransaction> Prepare(ICommand command)
            {
                return (context, transaction) => {
                    command.Execute(context, transaction);
                };
            }
        }
    }
    
    ブックファサードは、ファサードが実際にどのように見えるかの簡単な例です.再び、ファサードは、ビジネスロジックを含んでいません.彼らは単にDTsのDTOまたは論理的なグループ化に関連するクエリとコマンドをグループ化するのを許します.
    using System;
    using System.Collections.Generic;
    using System.Data;
    using dotnet_api_cqrs.contracts.data;
    using dotnet_api_cqrs.contracts.dto;
    using dotnet_api_cqrs.data.Commands.Book;
    using dotnet_api_cqrs.data.Queries.Book;
    
    namespace dotnet_api_cqrs.data
    {
        public class BookFacade : Facade, IBookFacade
        {
            public Func<IDbContext, IDbTransaction, IEnumerable<Book>> GetBooks()
            {
                return Prepare(new GetAllBooksQuery());
            }
    
            public Func<IDbContext, IDbTransaction, Book> GetBook(int bookID)
            {
                return Prepare(new GetBookQuery(bookID));
            }
    
            public Func<IDbContext, IDbTransaction, IEnumerable<Book>> GetBooksForAuthor(int authorID)
            {
                return Prepare(new GetBooksForAuthorQuery(authorID));
            }
    
            public Func<IDbContext, IDbTransaction, int> InsertBook(Book book)
            {
                return Prepare<int>(new InsertBookCommand(book));
            }
    
            public Action<IDbContext, IDbTransaction> DeleteBook(int bookID)
            {
                return Prepare(new DeleteBookCommand(bookID));
            }
        }
    }c#
    

    サービス


    サービスプロジェクトは、我々が起こる必要があるビジネスロジックを置くところです.DAPPERとCQRSと一緒に行ったときに見つけた1つのことは、私たちがデータベースから欲しかったものを正確に検索しているという事実によって、多くのビジネスロジックが気にかけられたということでした.データベース内の各テーブルとそのエンティティで動作するリポジトリを表すエンティティを持っていませんでした.我々は、我々が欲しかったコラムだけをしました、そして、我々は我々が実体フレームワークと倉庫パターンのすべてのボイラープレートなしで彼らを欲しがって、我々のビジネスモデルを正確に作成するために単純なテーブル結合をすることができました.実際のコードは、ストアドプロシージャを実行し、複数のデータセットを返すメソッドを持っていますが、シンプルのためにここには含まれませんでした.それで、結局、我々のサービスは通常かなりきれいに見えます、しかし、彼らはビジネスロジックが起こる場所として勤めます.サービスはすべて、読み取りと書き込みのために複数のデータベースコンテキストを取り込むことができる基本サービスクラスを拡張します.我々はここにファサードをサービスに注入します.この特定の例では、すべての読み取りと書き込みを行うコンテキストだけを使用します.

    DOTNET API CQRS。サービス/ブックサービス。cs


    using System.Collections.Generic;
    using dotnet_api_cqrs.contracts.data;
    using dotnet_api_cqrs.contracts.dto;
    using dotnet_api_cqrs.contracts.services;
    
    namespace dotnet_api_cqrs.services
    {
        public class BookService : Service, IBookService
        {
            private readonly IBookFacade _bookFacade;
    
            public BookService(IDbContext context, IBookFacade bookFacade) : base(context)
            {
                _bookFacade = bookFacade;
            }
    
            public IEnumerable<Book> GetAllBooks()
            {
                return _bookFacade.GetBooks()(Context, null);
            }
    
            public Book GetBook(int bookID)
            {
                return _bookFacade.GetBook(bookID)(Context, null);
            }
    
            public IEnumerable<Book> GetBooksForAuthor(int authorID)
            {
                return _bookFacade.GetBooksForAuthor(authorID)(Context, null);
            }
    
            public Book InsertBook(Book book)
            {
                var newBookID = _bookFacade.InsertBook(book)(Context, null);
                return _bookFacade.GetBook(newBookID)(Context, null);
            }
    
            public void DeleteBook(int bookID)
            {
                _bookFacade.DeleteBook(bookID)(Context, null);
            }
    
            public Book ReplaceBook(Book oldBook, Book newBook)
            {
                return Context.Transaction<Book>(_transaction => {
                    _bookFacade.DeleteBook(oldBook.BookID)(Context, _transaction);
    
                    var newBookID = _bookFacade.InsertBook(newBook)(Context, _transaction);
                    return _bookFacade.GetBook(newBookID)(Context, _transaction);
                });
            }
        }
    }
    

    注射


    私は、ここで超深く行きません.それぞれのデータおよびサービスプロジェクトには、インジェクションのファサードとサービスを設定する静的クラスが含まれます.これらのクラスは起動時に参照されます.すべての必要な注入を設定するAPIのCSファイル.

    API


    APIは、書籍や著者のいくつかの単純なcrudメソッドを行うためのエンドポイントを持つ2つのコントローラがあります.

    DOTNET API CQRS。API /コントローラ/ブックコントローラ。cs


    using System.Collections.Generic;
    using dotnet_api_cqrs.contracts.dto;
    using dotnet_api_cqrs.contracts.services;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    
    namespace dotnet_api_cqrs.api.Controllers
    {
        [ApiController]
        [Route("api/[controller]")]
        public class BookController : ControllerBase
        {
            private readonly ILogger<BookController> _logger;
            private readonly IBookService _bookService;
    
            public BookController(ILogger<BookController> logger, IBookService bookService)
            {
                _logger = logger;
                _bookService = bookService;
            }
    
            [HttpGet]
            public IEnumerable<Book> Get()
            {
                return _bookService.GetAllBooks();
            }
    
            [HttpGet("{bookID:int}")]
            public Book Get(int bookID)
            {
                return _bookService.GetBook(bookID);
            }
    
            [HttpGet("author/{authorID:int}")]
            public IEnumerable<Book> GetForAuthor(int authorID)
            {
                return _bookService.GetBooksForAuthor(authorID);
            }
    
            [HttpPost]
            public Book Post(Book book)
            {
                return _bookService.InsertBook(book);
            }
    
            [HttpDelete("{bookID:int}")]
            public IActionResult Delete(int bookID)
            {
                _bookService.DeleteBook(bookID);
                return Ok();
            }
        }
    }
    

    利益

  • このアプローチで見たパフォーマンスの上昇は印象的でした.250 msの戻り時間を見ていたところ、< 50 >に戻っていたクエリを書くことができました.
  • これ以上の狂ったEntity Framework/ODATAクエリが一緒にhobbledされました.ネットとボード全体のパフォーマンスを殺した.我々は非常に演奏された非常に最適化されたクエリを書くために私たちのデータベースチームで動作することができます.
  • このアプローチはテストを非常に簡素化し、開発速度を増加させました.
  • 安全なチームである
  • は、T字型の開発者には大きい.このアプローチは、私たちのデータベースエンジニアに簡単に我々の質問を理解して、それが「データベースなしエンジニア」ゾーンであるために使用する変更自体を作るためにコードに入る能力を許しました.
  • より多くの利益がありました、しかし、我々は現在ここにそれを残します.この記事は少し遅れている.
    これがAPIの設定の基本的な設定です.私はあなたの考え/質問/批判を取得したいと思います.優しい.これは私の最初の技術的なブログの投稿です.より多くの...