Entity Framework Core 1.0 unità di lavoro con middleware Asp.Net Core o filtro Mvc

asp.net-core-1.0 entity-framework-core middleware unit-of-work

Domanda

Sto usando EF Core 1.0 (precedentemente noto ad EF7) e ASP.NET Core 1.0 (precedentemente noto come ASP.NET 5) per un'API RESTful.

Mi piacerebbe avere qualche unità di lavoro portata a una richiesta http in modo tale che quando si risponde alla richiesta HTTP TUTTE le modifiche apportate a DbContext verranno salvate nel database, o nessuna verrà salvata (se ci fosse qualche eccezione, per esempio).

In passato ho utilizzato WebAPI2 per questo scopo con NHibernate utilizzando un filtro Azione in cui inizio la transazione sull'esecuzione dell'azione, e sull'azione eseguita termino la transazione e chiudo la sessione. Questo era il metodo raccomandato su http://isbn.directory/book/9781484201107

Comunque ora sto usando Asp.Net Core (con Asp.Net Core Mvc anche se questo non dovrebbe essere rilevante) e Entity Framework che, ho capito, implementa già un'unità di lavoro.

Penso che avere un middleware collegato alla pipeline ASP.NET (prima di MVC) sarebbe il modo giusto di fare le cose. Quindi una richiesta sarebbe:

PIPELINE ASP.NET: MyUnitOfWorkMiddleware ==> MVC Controller ==> Repository ==> MVC Controller ==> MyUnitOfWorkMiddleware

Stavo pensando di fare in modo che questo middleware salvi le modifiche di DbContext se non si è verificata alcuna eccezione, così che nelle implementazioni del mio repository non ho nemmeno bisogno di fare dbcontext.SaveChanges() e tutto sarebbe come una transazione centralizzata. In pseudocodice credo che sarebbe qualcosa di simile:

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
     }
}

Ha senso ciò? Qualcuno ha qualche esempio di qualcosa di simile? Non riesco a trovare alcuna buona pratica o raccomandazione in merito.

Inoltre, se seguo questo approccio al mio livello di controller MVC, non avrei accesso a nessun ID risorsa creato dal database durante il POST di una nuova risorsa perché l'ID non verrebbe generato finché le modifiche di dbContext non vengono salvate (in seguito nella pipeline nel mio middleware DOPO che il controller ha terminato l'esecuzione). Cosa succede se ho bisogno di accedere all'ID appena creato di una risorsa nel mio controller?

Qualsiasi consiglio sarebbe molto apprezzato!

AGGIORNAMENTO 1 : Ho riscontrato un problema con il mio approccio all'utilizzo del middleware per ottenere ciò poiché l'istanza di DbContext nel middleware non è la stessa durante la durata di MVC (e repository). Vedere la domanda Entity Framework Core 1.0 DbContext non con ambito per la richiesta http

AGGIORNAMENTO 2 : Non ho ancora trovato una buona soluzione. Fondamentalmente queste sono le mie opzioni finora:

  1. Salva le modifiche nel DB il prima possibile. Ciò significa salvarlo sull'implementazione del repository stesso. Il problema con questo approccio è che per una richiesta Http forse voglio usare diversi repository (ad esempio: salvare qualcosa nel database e poi caricare un blob in un cloud storage) e per avere una Unit of Work dovrei implementare un repository che si occupa di più di un'entità o anche di più di un metodo di persistenza (DB e Blob Storage), che sconfigge l'intero scopo
  2. Implementare un filtro azione in cui avvolgo l'intera esecuzione dell'azione in una transazione DB. Alla fine dell'esecuzione dell'azione del controllore, se non ci sono eccezioni, eseguo le ramificazioni su DB, ma se ci sono eccezioni, eseguo il rollback e scartare il contesto. Il problema con questo è che l'azione del mio controllore può richiedere un ID di Entità generato per restituirlo al client http (es .: Se ottengo un POST / api / auto vorrei restituire un 201 Accettato con un'intestazione di posizione che identifica la nuova risorsa creata in / api / cars / 123 e l'Id 123 non sarebbero ancora disponibili poiché l'entità non è stata salvata nel DB e l'Id è ancora un 0 temporaneo). Esempio di azione del controllore per una richiesta di verbo POST:

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

Come posso avere l'intera azione del controller racchiusa in una transazione DB e allo stesso tempo avere a disposizione qualsiasi ID generato dal database per restituirlo nella risposta Http dal controller? Oppure ... c'è un modo elegante per sovrascrivere la risposta http e impostare l'Id al livello del filtro di azione una volta che le modifiche del DB sono state commesse?

AGGIORNAMENTO 3: Come per il commento di nathanaldensr , ho potuto ottenere il meglio da entrambi i mondi (avvolgendo l'esecuzione dell'azione del mio controller in una transazione DB _ UoW e anche conoscendo l'Id della nuova risorsa creata ancor prima che il DB esegua le modifiche) utilizzando il codice generato I guidi si affidano invece al database per generare il Guid.

Risposta accettata

Anch'io sto affrontando lo stesso problema e non sono sicuro di quale approccio seguire. Uno degli approcci che ho usato è il seguente:

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();
    }
}

Risposta popolare

Come per Entity Framework Core 1.0 DbContext non ha ambito per la richiesta http Non ho potuto utilizzare un middleware per ottenere ciò poiché l'istanza di DbContext che il middleware viene iniettato non è la stessa di DbContext durante l'esecuzione di MVC (nei miei controller o repository).

Ho dovuto seguire un approccio simile per salvare le modifiche in DbContext dopo l'esecuzione dell'azione del controller utilizzando un filtro globale. Non esiste ancora una documentazione ufficiale sui filtri in MVC 6, quindi se qualcuno è interessato a questa soluzione vedi sotto il filtro e il modo in cui faccio questo filtro globale in modo che venga eseguito prima dell'azione di qualsiasi controller.

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");
        }
    }
}

e il filtro viene collegato al mio MVC su Startup.cs durante la configurazione di MVC.

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");
        }
    }
}

Questo lascia ancora una domanda (vedi AGGIORNAMENTO 2 sulla mia domanda). Cosa succede se desidero che il mio controller risponda a una richiesta HTTP POST con un'intestazione 201 Accepted with Location che include l'Id dell'entità creata nel DB? Quando l'azione del controllore termina l'esecuzione, le modifiche non sono ancora state commesse nel DB, pertanto l'ID dell'entità creata è ancora 0 finché il filtro azione non salva le modifiche e il DB genera un valore.




Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché