14

Korzystam z EF Core 1.0 (wcześniej znany jako EF7) i ASP.NET Core 1.0 (wcześniej znany jako ASP.NET 5) dla RESTful API .Jednostka Entity Framework Core 1.0 z oprogramowaniem warstwy pośredniej Asp.Net Core lub filtrem Mvc

Chciałbym, aby jakaś jednostka pracy była ustawiona na żądanie http w taki sposób, aby podczas odpowiadania na żądanie HTTP WSZYSTKIE zmiany wprowadzone do DbContext były zapisywane w bazie danych, lub żaden nie zostanie zapisany (jeśli był jakiś wyjątek, na przykład).

W przeszłości użyłem WebAPI2 do tego celu z NHibernate za pomocą filtra akcji, w którym rozpocząłem transakcję po wykonaniu akcji, a po wykonaniu operacji kończę transakcję i zamykam sesję. To był sposób zalecany pod adresem http://isbn.directory/book/9781484201107

Jednak teraz używam Asp.Net Core (z Asp.Net Core Mvc, chociaż nie powinno to być istotne) i Entity Framework, który, jak zrozumiałem, już implementuje jednostkę pracy.

Myślę, że posiadanie oprogramowania pośredniego podłączonego do potoku ASP.NET (przed MVC) byłoby właściwym sposobem robienia rzeczy. Więc wniosek pójdzie:

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

Myślałam o konieczności to middleware zapisać DbContext zmienia się, jeśli nie wystąpił wyjątek, więc w implementacjach mojego repozytorium nie muszę nawet wykonywać dbcontext.SaveChanges() i wszystko byłoby jak scentralizowana transakcja. W pseudokod Myślę, że to byłoby coś takiego:

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

Czy to ma sens? Czy ktokolwiek ma przykład czegoś podobnego? Nie mogę znaleźć żadnej dobrej praktyki lub rekomendacji w tym zakresie.

Ponadto, jeśli zastosuję to podejście na moim poziomie kontrolera MVC, nie będę miał dostępu do żadnego identyfikatora zasobu utworzonego przez bazę danych podczas POSTING nowego zasobu, ponieważ identyfikator nie zostanie wygenerowany, dopóki zmiany dbContext nie zostaną zapisane (później włączone w potoku w moim oprogramowaniu pośrednim po zakończeniu wykonywania przez kontroler). Co się stanie, jeśli będę potrzebował uzyskać dostęp do nowo utworzonego identyfikatora zasobu w moim kontrolerze?

Każda rada byłaby bardzo ceniona!

UPDATE 1: znalazłem problem z moim podejściem do wykorzystania middleware do osiągnięcia tego celu, ponieważ instancja DbContext w middleware nie jest taka sama jak podczas MVC (i repozytoriów) życia. Zobacz pytanie Entity Framework Core 1.0 DbContext not scoped to http request

UPDATE 2:ja jeszcze nie znalazłem dobre rozwiązanie. Zasadniczo są to moje dotychczasowe opcje:

  1. Zapisz zmiany w DB tak szybko, jak to możliwe. Oznacza to zapisanie go w samej implementacji repozytorium. Problem z tym podejściem polega na tym, że dla żądania HTTP mogę chcieć użyć kilku repozytoriów (np.e: zapisz coś w bazie danych, a następnie prześlij blob do pamięci w chmurze) i aby mieć Jednostkę Pracy, będę musiał zaimplementować repozytorium, które zajmuje się więcej niż jedną jednostką lub nawet więcej niż jedną metodą persistance (DB i Blob Storage), który pokonuje cały cel:
  2. Implementacja Action Filter, w której zawijam całe wykonanie akcji w transakcji DB. Na końcu wykonywania akcji kontrolera, jeśli nie ma wyjątków, to zgłaszam chance do DB, ale jeśli są wyjątki, wycofuję i odrzucam kontekst. Problem polega na tym, że działanie mojego kontrolera może wymagać wygenerowanego identyfikatora Entity w celu zwrócenia go do klienta http (tzn. Jeśli dostanę POST/api/samochody, chciałbym zwrócić 201 Zaakceptowany z nagłówkiem lokalizacji, który identyfikuje nowy zasób stworzony w/api/cars/123, a Id 123 nie byłby jeszcze dostępny, ponieważ encja nie została zapisana w DB, a Id jest nadal tymczasowy 0). Przykład w działaniu sterownika dla żądania POST czasownika:

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

Jak mogę mieć działanie całego regulatora owinięty w transakcji DB i jednocześnie mieć dostępny dowolny identyfikator generowany przez bazę danych w celu zwrócić go w odpowiedzi HTTP z kontrolera? Lub .. jest jakiś elegancki sposób na zastąpienie odpowiedzi http i ustawić Id na poziomie filtra akcji po zatwierdzeniu zmian DB?

UPDATE 3: Zgodnie nathanaldensr „s komentarzu mogę uzyskać najlepsze z obu światów (owijanie wykonanie działania mojego kontrolera w transakcji DB _ UOW, a także znając ID nowego zasobu stworzonego jeszcze przed DB zatwierdza zmiany), wykorzystując wygenerowany kod Guids zamiast polegać na bazie danych, aby wygenerować Guid.

Odpowiedz

5

Mam również do czynienia z tym samym problemem i nie wiem, które podejście do naśladowania. Jednym z zastosowanych przeze mnie rozwiązań jest:

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

Jest to dobre rozwiązanie dzięki – iberodev

+0

Podoba mi się, że można udekorować dowolną akcję lub kontroler za pomocą filtra jednostki pracy, aby zawinąć wszystko w transakcji podczas cyklu życia mvc. – iberodev

+0

Problem polega na tym, co należy zrobić, gdy potrzebny jest identyfikator jednostki, aby dalej wykonywać czynności w ramach tego samego żądania http. Identyfikator nie zostanie wygenerowany do czasu wprowadzenia zmian w DB. Przy takim podejściu zmiany nie są zapisywane, dopóki sterownik nie sfinalizuje wykonania. – iberodev

1

Moja rada, użyj dbContext.SaveChanges() w kontrolerze, jak to pokazano we wszystkich przykładach w Internecie. To, co chcesz robić, brzmi całkiem fantazyjnie i może być odwrotne, jak się domyślasz na końcu postu. I IMO, to nie ma sensu.

Odnośnie drugiego pytania/zadania:

.... kiedy w odpowiedzi na żądania HTTP albo wszystkie zmiany dokonane w DbContext zostaną zapisane do bazy danych lub żaden zostaną zapisane (jeśli istnieją był na przykład wyjątek).

Myślę, że potrzebujesz czegoś w rodzaju "transakcji za każde żądanie". To tylko pomysł, w ogóle go nie testowałem. Po prostu umieścić kod razem w tej próbki middleware:

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

Powodzenia

+0

Wiem, że jest to pokazane w kilku przykładach, ale to tylko przykłady. Dla prawdziwych dużych aplikacji n-warstwowych nie uważam, że dobrym pomysłem jest wstrzykiwanie DbContext do kontrolera, ponieważ łamie on rozdzielanie problemów i tworzy zależność między warstwą usługi a warstwą persistance (nawet zależność z EntityFramework) i to jest powód, dla którego używałem wzorca repozytorium. Obawiam się, że to nie jest poprawna odpowiedź :) – iberodev

+0

Mój DbContext jest już ograniczony do żądania HTTP i DbContext (ponieważ EF 6) jest transakcją podczas zapisywania zmian zgodnie z https://msdn.microsoft.com/en- us/data/dn456843.aspx , dlatego zastanawiałem się, czy oprogramowanie pośrednie było idealnym miejscem do zrobienia tylko raz SaveChanges (bez potrzeby jawnej transakcji). Ale pomysł jest podobny, tak. Zawijanie wszystkiego wewnątrz transakcji i zatwierdzanie transakcji (zapisywanie zmian kontekstu) po zakończeniu wykonywania przez kontroler – iberodev

+1

Możesz zaimplementować swój własny UnitOfWork wokół DbContext. Mój komentarz dotyczy metody SaveChanges, nie ma znaczenia, czy jest ona odsłonięta przez obiekt DbContext lub unitOfWork. Nie wyrażałem się poprawnie, ponieważ nie lubię wstrzykiwania kontekstu bezpośrednio do kontrolera. :) – regnauld

12

Zgodnie Entity Framework Core 1.0 DbContext not scoped to http request nie mogłem wykorzystać middleware do osiągnięcia tego celu, ponieważ instancja DbContext że middleware zostaje wstrzyknięty nie jest tak samo jak DbContext podczas wykonywania MVC (w moich kontrolerach lub repozytoriach).

Musiałem przejść z podobnym podejściem, aby zapisać zmiany w DbContext po wykonaniu działania kontrolera przy użyciu filtra globalnego. Nie ma oficjalnej dokumentacji na temat filtrów w MVC 6, więc jeśli ktokolwiek jest zainteresowany tym rozwiązaniem, zobacz poniżej filtr i sposób, w jaki robię ten filtr globalnie, tak aby był wykonywany przed jakimkolwiek działaniem kontrolera.

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

i filtr zostanie podłączony do mojego MVC w Startup.cs podczas konfigurowania 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)); 
     }); 
    //.. 
} 

Pozostaje pytanie (patrz AKTUALIZACJA 2 na moje pytanie). Co jeśli chcę, aby mój kontroler odpowiadał na żądanie http POST z nagłówkiem 201 Zaakceptowany z lokalizacją, który zawiera identyfikator podmiotu utworzonego w DB? Gdy działanie kontrolera kończy wykonywanie, zmiany nie zostały jeszcze zatwierdzone do bazy danych, dlatego identyfikator utworzonego obiektu jest wciąż 0, dopóki filtr akcji nie zapisze zmian, a DB wygeneruje wartość.

Powiązane problemy