web-dev-qa-db-ja.com

Asp.NetCoreミドルウェアまたはMvcフィルターを使用したEntityFramework Core1.0の作業単位

RESTfulAPIにEFCore 1.0(以前は広告EF7と呼ばれていました)とASP.NET Core 1.0(以前はASP.NET 5と呼ばれていました)を使用しています。

HTTPリクエストに応答するときに、DbContextに加えられたすべての変更がデータベースに保存されるか、何も保存されないように、一部の作業単位をhttpリクエストにスコープさせたいと思います(たとえば、いくつかの例外)。

過去に、NHibernateでこの目的のためにWebAPI2を使用し、アクションフィルターを使用して、アクションの実行時にトランザクションを開始し、実行されたアクションでトランザクションを終了してセッションを閉じました。これは http://isbn.directory/book/9781484201107 で推奨されている方法でした

ただし、現在、Asp.Net Core(Asp.Net Core Mvcを使用していますが、これは関係ありません)と、すでに作業単位を実装しているEntityFrameworkを使用しています。

ミドルウェアを(MVCの前に)ASP.NETパイプラインに接続するのが正しい方法だと思います。したがって、リクエストは次のようになります。

PIPELINE ASP.NET:MyUnitOfWorkMiddleware ==> MVCコントローラー==>リポジトリ==> MVCコントローラー==> MyUnitOfWorkMiddleware

例外が発生しなかった場合にこのミドルウェアにDbContextの変更を保存させることを考えていたので、リポジトリの実装ではdbcontext.SaveChanges()を実行する必要すらなく、すべてが集中型トランザクションのようになります。擬似コードでは、次のようになると思います。

_class MyUnitOfWorkMiddleware
{
     //..
     1-get an instance of DbContext for this request.
     try {
         2-await the next item in the pipeline.
         3-dbContext.SaveChanges();
     }
     catch (Exception e) {
         2.1-rollback changes (simply by ignoring context)
         2.2-return an http error response
     }
}
_

これは意味がありますか?誰かが似たような例を持っていますか?私はこれに関する良い習慣や推奨事項を見つけることができません。

また、MVCコントローラーレベルでこのアプローチを使用すると、dbContextの変更が保存されるまで(後でパイプラインで)IDが生成されないため、新しいリソースをPOSTするときにデータベースによって作成されたリソースIDにアクセスできません。コントローラが実行を終了した後の私のミドルウェアで)。コントローラで新しく作成されたリソースのIDにアクセスする必要がある場合はどうなりますか?

アドバイスをいただければ幸いです。

UPDATE 1ミドルウェアのDbContextインスタンスはそうではないため、ミドルウェアを使用してこれを達成するアプローチに問題が見つかりましたMVC(およびリポジトリ)の存続期間中と同じです。質問を参照してください Entity Framework Core 1.0 DbContextはhttpリクエストにスコープされていません

UPDATE 2私はまだ良い解決策を見つけていません。基本的にこれらはこれまでの私のオプションです:

  1. 変更をできるだけ早くDBに保存します。つまり、リポジトリの実装自体に保存するということです。このアプローチの問題は、Httpリクエストの場合、複数のリポジトリを使用したい場合があることです(つまり、データベースに何かを保存してから、BLOBをクラウドストレージにアップロードします)。作業ユニットを作成するには、実装する必要があります。複数のエンティティまたは複数の永続化メソッド(DBおよびBlob Storage)を処理するリポジトリであり、目的全体を無効にします
  2. アクションフィルター を実装します。ここで、アクションの実行全体をDBトランザクションでラップします。コントローラのアクション実行の最後に、例外がない場合はDBにチャンチをコミットしますが、例外がある場合はロールバックしてコンテキストを破棄します。これに伴う問題は、コントローラーのアクションがhttpクライアントに返すために生成されたエンティティのIDを必要とする可能性があることです(つまり、POST/api/carsを取​​得した場合に返したい)/api/cars/123で作成された新しいリソースを識別するロケーションヘッダーで201が受け入れられ、エンティティがDBに保存されておらず、IDが一時的な0であるため、ID123はまだ使用できません。 POST動詞要求に対するコントローラーのアクション:

    return CreatedAtRoute("GetCarById", new { carId= carSummaryCreated.Id }, carSummaryCreated); //carSummaryCreated.Id would be 0 until the changes are saved in DB

コントローラーのアクション全体をDBトランザクションでラップし、同時にデータベースによって生成されたIDを使用して、コントローラーからのHttp応答で返すにはどうすればよいですか?または..DBの変更がコミットされたら、http応答を上書きし、アクションフィルターレベルでIDを設定するエレガントな方法はありますか?

UPDATE 3:nathanaldensr のコメントによると、両方の長所を活用できました(コントローラーのアクション実行をDBトランザクション_UoWであり、データベースに依存してGUIDを生成する代わりに、コードで生成されたGUIDを使用することにより、DBが変更をコミットする前でも作成された新しいリソースのIDを認識します。

16
diegosasw

私も同じ問題に直面しており、どのアプローチに従うべきかわかりません。私が使用したアプローチの1つは次のとおりです。

public class UnitOfWorkFilter : ActionFilterAttribute
{
    private readonly AppDbContext _dbContext;

    public UnitOfWorkFilter(AppDbContext dbContext,)
    {
        _dbContext = dbContext;
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        if (!context.HttpContext.Request.Method.Equals("Post", StringComparison.OrdinalIgnoreCase))
            return;
        if (context.Exception == null && context.ModelState.IsValid)
        {
            _dbContext.Database.CommitTransaction();
        }
        else
        {
            _dbContext.Database.RollbackTransaction();
        }
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.HttpContext.Request.Method.Equals("Post", StringComparison.OrdinalIgnoreCase))
            return;
        _dbContext.Database.BeginTransaction();
    }
}
5
RahulSabharwal

Entity Framework Core 1.0 DbContextはhttpリクエストにスコープされていません ミドルウェアが注入されるDbContextのインスタンスは、MVC実行中のDbContextと同じではないため、ミドルウェアを使用してこれを実現できませんでした(私のコントローラー、またはリポジトリ)。

グローバルフィルターを使用してコントローラーのアクションを実行した後、DbContextの変更を保存するには、同様のアプローチを使用する必要がありました。 MVC 6のフィルターに関する公式ドキュメントはまだありません。このソリューションに興味がある場合は、以下のフィルターと、コントローラーのアクションの前に実行されるようにこのフィルターをグローバルにする方法を参照してください。

public class UnitOfWorkFilter : ActionFilterAttribute
{
    private readonly MyDbContext _dbContext;
    private readonly ILogger _logger;

    public UnitOfWorkFilter(MyDbContext dbContext, ILoggerFactory loggerFactory)
    {
        _dbContext = dbContext;
        _logger = loggerFactory.CreateLogger<UnitOfWorkFilter>();
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext executingContext, ActionExecutionDelegate next)
    {
        var executedContext = await next.Invoke(); //to wait until the controller's action finalizes in case there was an error
        if (executedContext.Exception == null)
        {
            _logger.LogInformation("Saving changes for unit of work");
            await _dbContext.SaveChangesAsync();
        }
        else
        {
            _logger.LogInformation("Avoid to save changes for unit of work due an exception");
        }
    }
}

そして、MVCを構成するときに、フィルターはStartup.csでMVCに接続されます。

public void ConfigureServices(IServiceCollection services)
{
   //..
   //Entity Framework 7
   services.AddEntityFramework()
            .AddSqlServer()
            .AddDbContext<SpeediCargoDbContext>(options => {
               options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
            });

        //MVC 6
        services.AddMvc(setup =>
        {
            setup.Filters.AddService(typeof(UnitOfWorkFilter));
        });
   //..
}

これはまだ質問を残します(私の質問のUPDATE 2を参照してください)。 コントローラーがhttp POSTリクエストに、DBで作成されたエンティティのIDを含む201 Accepted with Locationヘッダーで応答するようにするにはどうすればよいですか? コントローラーのアクションが実行を終了するとき、変更はまだDBにコミットされていないため、アクションフィルターが変更を保存してDBが値を生成するまで、作成されたエンティティのIDは0のままです。

13
diegosasw

私のアドバイスは、Web上のすべての例で示されているように、コントローラーでdbContext.SaveChanges()を使用することです。あなたがやりたいことはかなり派手に聞こえ、あなたがあなたの投稿の終わりに推測したように裏目に出る可能性があります。そしてIMO、それは意味がありません。

2番目の質問/タスクについて:

.... HTTPリクエストに応答すると、DbContextに加えられたすべての変更がデータベースに保存されるか、何も保存されません(たとえば、何らかの例外があった場合)。

「リクエストごとのトランザクション」のようなものが必要だと思います。それは単なるアイデアであり、まったくテストしていません。このサンプルミドルウェアにコードをまとめました。

public class TransactionPerRequestMiddleware
{
    private readonly RequestDelegate next_;

    public TransactionPerRequestMiddleware(RequestDelegate next)
    {
        next_ = next;
    }

    public async Task Invoke(HttpContext context, DbContext dbContext)
    {
        var transaction = dbContext.Database.BeginTransaction(
            System.Data.IsolationLevel.ReadCommitted);

        await next_.Invoke(context);

        if (context.Response.StatusCode == 200)
        {
            transaction.Commit();
        }
        else
        {
            transaction.Rollback();
        }
    }
}

幸運を

1
regnauld