From 444699158e32e9fb714bd9d54ce95d16167cd08d Mon Sep 17 00:00:00 2001 From: xhafan Date: Thu, 14 Jun 2018 13:46:40 +0200 Subject: [PATCH] Fixed error handling in the unit of work middleware - now it properly rolls back DB transaction. Allowing to pass isolation level into the middleware (created a new issue for Windsor Castle - https://github.com/castleproject/Windsor/issues/411). --- .../EmailMaker.Website.csproj | 4 +- .../TransactionScopeUnitOfWorkHttpModule.cs | 6 ++- .../{ => HttpModules}/UnitOfWorkHttpModule.cs | 9 ++-- src/EmailMaker.Website/Web.config | 4 +- .../TransactionScopeUnitOfWorkMiddleware.cs | 41 ++++++++++++------- .../Middleware/UnitOfWorkMiddleware.cs | 38 ++++++++++------- .../WindsorRegistrationExtensions.cs | 30 ++++++++++++++ src/EmailMaker.WebsiteCore/Startup.cs | 13 +++--- 8 files changed, 101 insertions(+), 44 deletions(-) rename src/EmailMaker.Website/{ => HttpModules}/TransactionScopeUnitOfWorkHttpModule.cs (92%) rename src/EmailMaker.Website/{ => HttpModules}/UnitOfWorkHttpModule.cs (86%) create mode 100644 src/EmailMaker.WebsiteCore/Middleware/WindsorRegistrationExtensions.cs diff --git a/src/EmailMaker.Website/EmailMaker.Website.csproj b/src/EmailMaker.Website/EmailMaker.Website.csproj index 31123bd..bad2551 100644 --- a/src/EmailMaker.Website/EmailMaker.Website.csproj +++ b/src/EmailMaker.Website/EmailMaker.Website.csproj @@ -233,8 +233,8 @@ Global.asax - - + + diff --git a/src/EmailMaker.Website/TransactionScopeUnitOfWorkHttpModule.cs b/src/EmailMaker.Website/HttpModules/TransactionScopeUnitOfWorkHttpModule.cs similarity index 92% rename from src/EmailMaker.Website/TransactionScopeUnitOfWorkHttpModule.cs rename to src/EmailMaker.Website/HttpModules/TransactionScopeUnitOfWorkHttpModule.cs index b8c111d..e214cd6 100644 --- a/src/EmailMaker.Website/TransactionScopeUnitOfWorkHttpModule.cs +++ b/src/EmailMaker.Website/HttpModules/TransactionScopeUnitOfWorkHttpModule.cs @@ -6,13 +6,15 @@ using CoreUtils.Storages; using Rebus.TransactionScopes; -namespace EmailMaker.Website +namespace EmailMaker.Website.HttpModules { // register TransactionScopeUnitOfWorkHttpModule in the web.config (system.webServer -> modules) // transaction scope is needed to send messages to EmailMaker service only when the DB transaction successfully commits // https://stackoverflow.com/a/8169117/379279 public class TransactionScopeUnitOfWorkHttpModule : IHttpModule { + private const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted; + public void Init(HttpApplication application) { application.BeginRequest += Application_BeginRequest; @@ -66,7 +68,7 @@ private TransactionScope GetTransactionScopePerWebRequest() { var newTransactionScope = new TransactionScope( TransactionScopeOption.Required, - new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted}, + new TransactionOptions {IsolationLevel = DefaultIsolationLevel}, TransactionScopeAsyncFlowOption.Enabled ); transactionScopeStoragePerWebRequest.Set(newTransactionScope); diff --git a/src/EmailMaker.Website/UnitOfWorkHttpModule.cs b/src/EmailMaker.Website/HttpModules/UnitOfWorkHttpModule.cs similarity index 86% rename from src/EmailMaker.Website/UnitOfWorkHttpModule.cs rename to src/EmailMaker.Website/HttpModules/UnitOfWorkHttpModule.cs index 279d7c2..1eba1bd 100644 --- a/src/EmailMaker.Website/UnitOfWorkHttpModule.cs +++ b/src/EmailMaker.Website/HttpModules/UnitOfWorkHttpModule.cs @@ -1,14 +1,17 @@ using System; +using System.Data; using System.Web; using CoreDdd.Domain.Events; using CoreDdd.UnitOfWorks; using CoreIoC; -namespace EmailMaker.Website +namespace EmailMaker.Website.HttpModules { // register UnitOfWorkHttpModule in the web.config (system.webServer -> modules) public class UnitOfWorkHttpModule : IHttpModule { + private const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted; + public void Init(HttpApplication application) { DomainEvents.EnableDelayedDomainEventHandling(); // messages sent from domain event handlers would not be sent if the main DB transaction rolls back @@ -21,7 +24,7 @@ public void Init(HttpApplication application) private void Application_BeginRequest(Object source, EventArgs e) { var unitOfWork = GetUnitOfWorkPerWebRequest(); - unitOfWork.BeginTransaction(); + unitOfWork.BeginTransaction(DefaultIsolationLevel); } private void Application_EndRequest(Object source, EventArgs e) @@ -40,7 +43,7 @@ private void _DomainEventHandlingSurroundingTransaction(Action domainEventHandli try { - unitOfWork.BeginTransaction(); + unitOfWork.BeginTransaction(DefaultIsolationLevel); domainEventHandlingAction(); diff --git a/src/EmailMaker.Website/Web.config b/src/EmailMaker.Website/Web.config index fcef6fa..ae0c122 100644 --- a/src/EmailMaker.Website/Web.config +++ b/src/EmailMaker.Website/Web.config @@ -50,8 +50,8 @@ - - + + diff --git a/src/EmailMaker.WebsiteCore/Middleware/TransactionScopeUnitOfWorkMiddleware.cs b/src/EmailMaker.WebsiteCore/Middleware/TransactionScopeUnitOfWorkMiddleware.cs index 2e42658..45b1201 100644 --- a/src/EmailMaker.WebsiteCore/Middleware/TransactionScopeUnitOfWorkMiddleware.cs +++ b/src/EmailMaker.WebsiteCore/Middleware/TransactionScopeUnitOfWorkMiddleware.cs @@ -12,13 +12,28 @@ namespace EmailMaker.WebsiteCore.Middleware // https://stackoverflow.com/a/8169117/379279 public class TransactionScopeUnitOfWorkMiddleware : IMiddleware { + private readonly IsolationLevel _isolationLevel; + + public TransactionScopeUnitOfWorkMiddleware(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) + { + _isolationLevel = isolationLevel; + } + public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - _BeginRequest(); + try + { + _BeginRequest(); - await next.Invoke(context); + await next.Invoke(context); - _EndRequest(); + _EndRequest(); + } + catch + { + _HandleErrorInRequest(); + throw; + } } private void _BeginRequest() @@ -32,8 +47,6 @@ private void _BeginRequest() private void _EndRequest() { - //if (HttpContext.Current.Server.GetLastError() != null) return; // todo: check out asp.net core error handling - var unitOfWork = GetUnitOfWorkPerWebRequest(); unitOfWork.Commit(); @@ -42,14 +55,14 @@ private void _EndRequest() transactionScope.Dispose(); } - // private void Application_Error(Object source, EventArgs e) // todo: check out asp.net core error handling - // { - // var unitOfWork = GetUnitOfWorkPerWebRequest(); - // unitOfWork.Rollback(); - // - // var transactionScope = GetTransactionScopePerWebRequest(); - // transactionScope.Dispose(); - // } + private void _HandleErrorInRequest() + { + var unitOfWork = GetUnitOfWorkPerWebRequest(); + unitOfWork.Rollback(); + + var transactionScope = GetTransactionScopePerWebRequest(); + transactionScope.Dispose(); + } private IUnitOfWork GetUnitOfWorkPerWebRequest() { @@ -63,7 +76,7 @@ private TransactionScope GetTransactionScopePerWebRequest() { var newTransactionScope = new TransactionScope( TransactionScopeOption.Required, - new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted}, + new TransactionOptions {IsolationLevel = _isolationLevel}, TransactionScopeAsyncFlowOption.Enabled ); transactionScopeStorage.Set(newTransactionScope); diff --git a/src/EmailMaker.WebsiteCore/Middleware/UnitOfWorkMiddleware.cs b/src/EmailMaker.WebsiteCore/Middleware/UnitOfWorkMiddleware.cs index f422499..3e6243a 100644 --- a/src/EmailMaker.WebsiteCore/Middleware/UnitOfWorkMiddleware.cs +++ b/src/EmailMaker.WebsiteCore/Middleware/UnitOfWorkMiddleware.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using System.Threading.Tasks; using CoreDdd.Domain.Events; using CoreDdd.UnitOfWorks; @@ -9,30 +10,39 @@ namespace EmailMaker.WebsiteCore.Middleware { public class UnitOfWorkMiddleware : IMiddleware { - public UnitOfWorkMiddleware() + private readonly IsolationLevel _isolationLevel; + + public UnitOfWorkMiddleware(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) { + _isolationLevel = isolationLevel; DomainEvents.EnableDelayedDomainEventHandling(); // make sure messages sent from domain event handlers will not be sent if the main DB transaction rolls back } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - _BeginRequest(); + try + { + _BeginRequest(); - await next.Invoke(context); + await next.Invoke(context); - _EndRequest(); + _EndRequest(); + } + catch + { + _HandleErrorInRequest(); + throw; + } } private void _BeginRequest() { var unitOfWork = GetUnitOfWorkPerWebRequest(); - unitOfWork.BeginTransaction(); + unitOfWork.BeginTransaction(_isolationLevel); } private void _EndRequest() { - //if (HttpContext.Current.Server.GetLastError() != null) return; // todo: check out asp.net core error handling - var unitOfWork = GetUnitOfWorkPerWebRequest(); unitOfWork.Commit(); @@ -45,7 +55,7 @@ private void _DomainEventHandlingSurroundingTransaction(Action domainEventHandli try { - unitOfWork.BeginTransaction(); + unitOfWork.BeginTransaction(_isolationLevel); domainEventHandlingAction(); @@ -57,12 +67,12 @@ private void _DomainEventHandlingSurroundingTransaction(Action domainEventHandli throw; } } - // - // private void Application_Error(Object source, EventArgs e) // todo: check out asp.net core error handling - // { - // var unitOfWork = GetUnitOfWorkPerWebRequest(); - // unitOfWork.Rollback(); - // } + + private void _HandleErrorInRequest() + { + var unitOfWork = GetUnitOfWorkPerWebRequest(); + unitOfWork.Rollback(); + } private IUnitOfWork GetUnitOfWorkPerWebRequest() { diff --git a/src/EmailMaker.WebsiteCore/Middleware/WindsorRegistrationExtensions.cs b/src/EmailMaker.WebsiteCore/Middleware/WindsorRegistrationExtensions.cs new file mode 100644 index 0000000..14e1a3b --- /dev/null +++ b/src/EmailMaker.WebsiteCore/Middleware/WindsorRegistrationExtensions.cs @@ -0,0 +1,30 @@ +using Castle.MicroKernel.Registration; +using Castle.Windsor; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace EmailMaker.WebsiteCore +{ + public static class WindsorRegistrationExtensions + { + // method taken from https://github.com/fir3pho3nixx/Windsor/blob/aspnet-core-windsor-final/src/Castle.Facilities.AspNetCore/WindsorRegistrationExtensions.cs + // this version allows to pass parameters into the middleware + public static void UseMiddlewareFromWindsor(this IApplicationBuilder app, IWindsorContainer container, object argumentsAsAnonymousType) + where T : class, IMiddleware + { + container.Register(Component.For()); + app.Use(async (context, next) => + { + var resolve = container.Resolve(argumentsAsAnonymousType); + try + { + await resolve.InvokeAsync(context, async (ctx) => await next()); + } + finally + { + container.Release(resolve); + } + }); + } + } +} \ No newline at end of file diff --git a/src/EmailMaker.WebsiteCore/Startup.cs b/src/EmailMaker.WebsiteCore/Startup.cs index e8bc988..ab594ab 100644 --- a/src/EmailMaker.WebsiteCore/Startup.cs +++ b/src/EmailMaker.WebsiteCore/Startup.cs @@ -4,7 +4,6 @@ using System.Data.SQLite; using System.IO; using System.Reflection; -using System.Transactions; using Castle.Facilities.AspNetCore; using Castle.MicroKernel.Registration; using Castle.Windsor; @@ -37,7 +36,7 @@ namespace EmailMaker.WebsiteCore { - public class Startup + public class Startup { private readonly WindsorContainer _windsorContainer = new WindsorContainer(); @@ -68,9 +67,6 @@ public IServiceProvider ConfigureServices(IServiceCollection services) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { - app.UseMiddlewareFromWindsor(_windsorContainer); - //app.UseMiddlewareFromWindsor(_windsorContainer); - if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -80,6 +76,9 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env) app.UseExceptionHandler("/Home/Error"); } + app.UseMiddlewareFromWindsor(_windsorContainer, new { isolationLevel = IsolationLevel.ReadCommitted }); + //app.UseMiddlewareFromWindsor(_windsorContainer, new { isolationLevel = IsolationLevel.ReadCommitted }); + app.UseStaticFiles(); //app.UseAuthentication(); @@ -118,8 +117,8 @@ private void _RegisterApplicationComponents() void _registerTransactionScopeStoragePerWebRequest() { _windsorContainer.Register( - Component.For>() - .ImplementedBy>() + Component.For>() + .ImplementedBy>() .LifestyleScoped()); }