14

Я использую EF Core 1.0 (ранее известное объявление EF7) и ASP.NET Core 1.0 (ранее известный как ASP.NET 5) для RESTful API ,Блок управления Entity Framework Core 1.0 с промежуточным программным обеспечением Asp.Net Core или фильтром Mvc

Я бы хотел, чтобы часть работы была привязана к HTTP-запросу таким образом, что при ответе на HTTP-запрос либо все изменения, внесенные в DbContext, будут сохранены в базе данных, либо никто не будет сохранен (например, если было какое-то исключение).

В прошлом я использовал WebAPI2 для этой цели с помощью NHibernate с помощью фильтра действий, в котором я начинаю транзакцию при выполнении действия, и при выполнении действия завершаю транзакцию и закрываю сеанс. Это было рекомендовано в http://isbn.directory/book/9781484201107

Однако теперь я использую Asp.Net Core (с Asp.Net Core Mvc, хотя это не должно быть релевантно) и Entity Framework, который, я понял, уже реализует единицу работы.

Я думаю, что наличие промежуточного программного обеспечения, подключенного к конвейеру ASP.NET (до MVC), было бы правильным путем. Таким образом, запрос будет идти:

PIPELINE ASP.NET: MyUnitOfWorkMiddleware ==> MVC контроллер ==> Repository ==> 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 не будут сохранены (позже в конвейере в моем промежуточном ПО ПОСЛЕ завершения работы контроллера). Что делать, если мне нужно получить доступ к вновь созданному идентификатору ресурса в моем контроллере?

Любой совет будет рад!

UPDATE 1: Я нашел проблему с моим подходом использовать промежуточное программное обеспечение для достижения этой цели, так как экземпляр DbContext в ПО промежуточного слоя не такой же, как во время MVC (и хранилищ) жизни. Смотрите вопрос Entity Framework Core 1.0 DbContext not scoped to http request

UPDATE 2: Я еще не нашел хорошее решение. В основном это мои варианты до сих пор:

  1. Сохраните изменения в БД как можно скорее. Это означает, что он сохраняет его в самой реализации репозитория. Проблема с этим подходом заключается в том, что для запроса Http, возможно, я хочу использовать несколько репозиториев (т.е.e: сохраните что-нибудь в базе данных, а затем загрузите blob в облачное хранилище), и для того, чтобы иметь единицу работы, мне пришлось бы реализовать репозиторий, который имеет дело с несколькими объектами или даже с более чем одним методом persistance (DB и Blob Хранение), которая побеждает целую цель
  2. Внесите Action Filter, где я завершаю выполнение всего действия в транзакции БД. В конце выполнения действия контроллера, если нет исключений, я беру chanches в DB, ​​но если есть исключения, я откатываюсь и отбрасываю контекст. Проблема в том, что для действия моего контроллера может потребоваться сформированный Идентификатор Entity, чтобы вернуть его клиенту http (то есть: если я получу POST/api/cars, я хотел бы вернуть 201 Accepted с заголовком местоположения, который идентифицирует новый ресурс, созданный в/api/cars/123 и Id 123, еще не будет доступен, поскольку объект не был сохранен в БД, а идентификатор все еще является временным 0). Пример в действии контроллера для запроса POST глагола:

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

Как я мог бы иметь действие на целом диспетчерском завернутом в транзакции БДА и в то же время иметь в своем распоряжении любого Id генерируемой базы данных в порядке вернуть его в Http Response от контроллера? Или .. Есть ли какой-либо элегантный способ переписать ответ HTTP и установить Id на уровне фильтра действий после того, как были изменены изменения БД?

UPDATE 3: Согласно nathanaldensr «s комментарий я мог бы получить лучшее из обоих миров (оберточной выполнение действия моего контроллера в транзакции БД _ UOW, а также зная идентификатор нового ресурса, созданного еще до БД фиксирует изменения) с помощью генерируемых кода. Гиды вместо этого полагаются на базу данных для создания Guid.

ответ

5

Я также сталкиваюсь с той же проблемой и не уверен, какой подход следовать. Одним из подхода, который я использовал это следующим образом:

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

Это хорошее решение благодаря – iberodev

+0

Мне нравится, что вы можете украсить любое действие или контроллер с помощью блока рабочего фильтра, чтобы обернуть все в транзакции в течение жизненного цикла mvc. – iberodev

+0

Проблема остается в том, что делать, когда вам нужен идентификатор объекта, чтобы продолжать делать вещи в рамках одного и того же HTTP-запроса. Идентификатор не будет генерироваться до тех пор, пока изменения в БД не будут выполнены. И при таком подходе изменения не сохраняются до тех пор, пока контроллер не завершит выполнение. – iberodev

1

Мой совет, используйте dbContext.SaveChanges() в контроллере, как это показано во всех примерах через Интернет. То, что вы хотите сделать, звучит довольно интересно и может иметь неприятные последствия, как вы догадались в конце своего сообщения. И ИМО, это не имеет смысла.

Что касается вашего второго вопроса/задачи:

.... при ответе на запрос 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(); 
     } 
    } 
} 

Удачи

+0

Я знаю, что показано на некоторых примерах, но они всего лишь примеры. Для реальных больших n-ярусных приложений я не думаю, что это хорошая идея - ввести DbContext на контроллер, поскольку он нарушает разделение проблем и создает зависимость между уровнем обслуживания и уровнем безопасности (даже зависимость от EntityFramework) и вот почему я использовал шаблон репозитория. Я боюсь, что это неверный ответ :) – iberodev

+0

Мой DbContext уже привязан к запросу Http и DbContext (поскольку EF 6) является транзакцией при сохранении изменений в соответствии с https://msdn.microsoft.com/en- us/data/dn456843.aspx Вот почему я задавался вопросом, было ли промежуточное ПО идеальным местом для работы только после SaveChanges (без необходимости явной транзакции). Но идея такая же, да. Обертывание всего внутри транзакции и совершение транзакции (сохранение изменений контекста) после завершения выполнения контроллером – iberodev

+1

Вы можете реализовать свой собственный UnitOfWork вокруг DbContext. Мой комментарий был для метода SaveChanges, неважно, будет ли он открыт DbContext или ваш объект unitOfWork. Я не проявил себя правильно, так как мне также не нравится вводить контекст непосредственно в контроллер. :) – regnauld

12

По Entity Framework Core 1.0 DbContext not scoped to http request я не мог использовать промежуточное программное обеспечение для достижения этой цели, так как экземпляр DbContext, что промежуточное получает инъекции не то же, что и DbContext во время выполнения MVC (в моих контроллерах или репозиториях).

Мне пришлось пойти аналогичным образом, чтобы сохранить изменения в 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)); 
     }); 
    //.. 
} 

Это все еще оставляет вопрос (см. ОБНОВЛЕНИЕ 2 по моему вопросу). Что делать, если я хочу, чтобы мой контроллер отвечал на запрос http POST с заголовком 201 Accepted with the Location, который включает в себя идентификатор объекта, созданного в БД? Когда действие контроллера завершается, изменения еще не были зафиксированы в БД, поэтому идентификатор созданного объекта по-прежнему равен 0, пока фильтр действий не сохранит изменения, и БД генерирует значение.