├── .gitignore ├── Community.AspNetCore.ExceptionHandling.Integration ├── Community.AspNetCore.ExceptionHandling.Integration.csproj ├── Controllers │ └── ValuesController.cs ├── Program.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── Community.AspNetCore.ExceptionHandling.Mvc ├── Community.AspNetCore.ExceptionHandling.Mvc.csproj └── PolicyBuilderExtensions.cs ├── Community.AspNetCore.ExceptionHandling.NewtonsoftJson ├── Community.AspNetCore.ExceptionHandling.NewtonsoftJson.csproj └── PolicyBuilderExtensions.cs ├── Community.AspNetCore.ExceptionHandling.Tests ├── Community.AspNetCore.ExceptionHandling.Tests.csproj ├── Exc │ └── ReThrowExceptionHandlerTests.cs ├── Handlers │ ├── MarkHandledHandlerTests.cs │ └── NextChainHandlerTests.cs └── Scenarious │ ├── Exceptions.cs │ ├── ScenariosTests.cs │ └── Startup.cs ├── Community.AspNetCore.ExceptionHandling ├── AppBuilderExtensions.cs ├── Builder │ ├── ExceptionMapping.cs │ ├── IExceptionMapping.cs │ └── PolicyBuilder.cs ├── Community.AspNetCore.ExceptionHandling.csproj ├── Const.cs ├── Events.cs ├── Exc │ └── ReThrowExceptionHandler.cs ├── ExceptionHandlingPolicyMiddleware.cs ├── ExceptionHandlingPolicyOptions.cs ├── Handlers │ ├── HandlerResult.cs │ ├── HandlerStrongType.cs │ ├── HandlerWithLogger.cs │ ├── HandlerWithLoggerOptions.cs │ ├── MarkHandledHandler.cs │ └── NextChainHandler.cs ├── IExceptionHandler.cs ├── IExceptionPolicyBuilder.cs ├── Logs │ ├── DisableLoggingHandler.cs │ ├── LogExceptionHandler.cs │ └── LogHandlerOptions.cs ├── PolicyBuilderExtensions.cs ├── Response │ ├── RawResponseExceptionHandler.cs │ └── RawResponseHandlerOptions.cs ├── ResponseAlreadyStartedBehaviour.cs └── Retry │ ├── RetryHandler.cs │ └── RetryHandlerOptions.cs ├── Community.AspNetCore.sln ├── LICENSE ├── README.md ├── Transitions.png ├── Transitions.vsdx ├── build.ps1 └── sgn.snk /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Integration/Community.AspNetCore.ExceptionHandling.Integration.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1;netcoreapp3.0;netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Integration/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Community.AspNetCore.ExceptionHandling.Integration.Controllers 7 | { 8 | [Route("api/[controller]")] 9 | public class ValuesController : ControllerBase 10 | { 11 | // GET api/values 12 | [HttpGet] 13 | public IEnumerable Get() 14 | { 15 | return new string[] { "value1", "value2" }; 16 | } 17 | 18 | // GET api/values/5 19 | [HttpGet("{id}")] 20 | public string Get(int id) 21 | { 22 | 23 | if (id > 25) 24 | { 25 | throw new DuplicateWaitObjectException(); 26 | } 27 | 28 | if (id > 20) 29 | { 30 | throw new DuplicateNameException(); 31 | } 32 | 33 | if (id > 15) 34 | { 35 | throw new InvalidConstraintException(); 36 | } 37 | 38 | if (id > 10) 39 | { 40 | throw new ArgumentOutOfRangeException(); 41 | } 42 | 43 | if (id > 5) 44 | { 45 | throw new InvalidCastException(); 46 | } 47 | 48 | return "value"; 49 | } 50 | 51 | // POST api/values 52 | [HttpPost] 53 | public void Post([FromBody]string value) 54 | { 55 | } 56 | 57 | // PUT api/values/5 58 | [HttpPut("{id}")] 59 | public void Put(int id, [FromBody]string value) 60 | { 61 | } 62 | 63 | // DELETE api/values/5 64 | [HttpDelete("{id}")] 65 | public void Delete(int id) 66 | { 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Integration/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace Community.AspNetCore.ExceptionHandling.Integration 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup() 16 | .Build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Integration/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text; 4 | using Community.AspNetCore.ExceptionHandling.Mvc; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Community.AspNetCore.ExceptionHandling.Integration 12 | { 13 | public class Startup 14 | { 15 | public Startup(IConfiguration configuration) 16 | { 17 | Configuration = configuration; 18 | } 19 | 20 | public IConfiguration Configuration { get; } 21 | 22 | // This method gets called by the runtime. Use this method to add services to the container. 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | #if NETCOREAPP3_0 26 | services.AddMvc(options => options.EnableEndpointRouting = false); 27 | #else 28 | services.AddMvc(); 29 | #endif 30 | 31 | services.AddExceptionHandlingPolicies(options => 32 | { 33 | options.For().Retry().NextPolicy(); 34 | 35 | options.For().Retry(); 36 | 37 | options.For().Log().Rethrow(); 38 | 39 | options.For() 40 | .Response(e => 400) 41 | .Headers((h, e) => h["X-qwe"] = e.Message) 42 | .WithBody((req,sw, exception) => 43 | { 44 | byte[] array = Encoding.UTF8.GetBytes(exception.ToString()); 45 | return sw.WriteAsync(array, 0, array.Length); 46 | }) 47 | .NextPolicy(); 48 | 49 | options.For() 50 | .Log(lo => { lo.LogAction = (l, c, e) => l.LogError(e,"qwe"); }) 51 | //.Response(e => 500, ResponseAlreadyStartedBehaviour.GoToNextHandler).ClearCacheHeaders().WithBodyJson((r, e) => new { msg = e.Message, path = r.Path }) 52 | .Response(e => 500, ResponseAlreadyStartedBehaviour.GoToNextHandler).ClearCacheHeaders().WithObjectResult((r, e) => new { msg = e.Message, path = r.Path }) 53 | .Handled(); 54 | }); 55 | 56 | services.AddLogging(); 57 | } 58 | 59 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 60 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 61 | { 62 | //app.UseResponseBuffering(); 63 | app.UseExceptionHandlingPolicies(); 64 | app.UseMvc(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Integration/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Integration/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Mvc/Community.AspNetCore.ExceptionHandling.Mvc.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Extension methods to configure exception handler which write MVC action result to responce body. Userfull for writing objects 6 | https://github.com/IharYakimush/asp-net-core-exception-handling 7 | https://github.com/IharYakimush/asp-net-core-exception-handling/blob/develop/LICENSE 8 | IharYakimush 9 | aspnetcore exception handling policy mvc action result 10 | 2.2.0.0 11 | true 12 | true 13 | 2.2.0.0 14 | 15 | IharYakimush 16 | 2.2.0 17 | true 18 | ..\sgn.snk 19 | 20 | Library 21 | 22 | Fixed compatibility with AspNetCore 2.2 and AspNetCore 3.0 for log handler. Breaking changes in log handler configuration. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Mvc/PolicyBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Community.AspNetCore.ExceptionHandling.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Abstractions; 6 | using Microsoft.AspNetCore.Mvc.Infrastructure; 7 | using Microsoft.AspNetCore.Routing; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace Community.AspNetCore.ExceptionHandling.Mvc 11 | { 12 | public static class PolicyBuilderExtensions 13 | { 14 | private static readonly RouteData EmptyRouteData = new RouteData(); 15 | 16 | private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); 17 | 18 | /// 19 | /// Set to response and pass control to next handler. 20 | /// 21 | /// 22 | /// The exception type 23 | /// 24 | /// 25 | /// The action result type. Should implement . 26 | /// 27 | /// 28 | /// The policy builder 29 | /// 30 | /// 31 | /// The factory. 32 | /// 33 | /// 34 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 35 | /// 36 | /// 37 | /// Policy builder 38 | /// 39 | public static IResponseHandlers WithActionResult( 40 | this IResponseHandlers builder, Func resultFactory, int index = -1) 41 | where TException : Exception 42 | where TResult : IActionResult 43 | { 44 | return builder.WithBody((request, streamWriter, exception) => 45 | { 46 | var context = request.HttpContext; 47 | var executor = context.RequestServices.GetService>(); 48 | 49 | if (executor == null) 50 | { 51 | throw new InvalidOperationException($"No result executor for '{typeof(TResult).FullName}' has been registered."); 52 | } 53 | 54 | var routeData = context.GetRouteData() ?? EmptyRouteData; 55 | 56 | var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor); 57 | 58 | return executor.ExecuteAsync(actionContext, resultFactory(request, exception)); 59 | }); 60 | } 61 | 62 | /// 63 | /// Set to response and pass control to next handler. 64 | /// 65 | /// 66 | /// The exception type 67 | /// 68 | /// 69 | /// The action result type. Should implement . 70 | /// 71 | /// 72 | /// The policy builder 73 | /// 74 | /// 75 | /// The action result. 76 | /// 77 | /// 78 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 79 | /// 80 | /// 81 | /// Policy builder 82 | /// 83 | public static IResponseHandlers WithActionResult( 84 | this IResponseHandlers builder, TResult result, int index = -1) 85 | where TException : Exception 86 | where TResult : IActionResult 87 | { 88 | return WithActionResult(builder, (request, exception) => result); 89 | } 90 | 91 | /// 92 | /// Set to response and pass control to next handler. 93 | /// 94 | /// 95 | /// The exception type 96 | /// 97 | /// 98 | /// The result object type. 99 | /// 100 | /// 101 | /// The policy builder 102 | /// 103 | /// 104 | /// The result object. 105 | /// 106 | /// 107 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 108 | /// 109 | /// 110 | /// Policy builder 111 | /// 112 | public static IResponseHandlers WithObjectResult( 113 | this IResponseHandlers builder, TObject value, int index = -1) 114 | where TException : Exception 115 | { 116 | return WithActionResult(builder, new ObjectResult(value), index); 117 | } 118 | 119 | /// 120 | /// Set to response and pass control to next handler. 121 | /// 122 | /// 123 | /// The exception type 124 | /// 125 | /// 126 | /// The result object type. 127 | /// 128 | /// 129 | /// The policy builder 130 | /// 131 | /// 132 | /// The result object factory. 133 | /// 134 | /// 135 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 136 | /// 137 | /// 138 | /// Policy builder 139 | /// 140 | public static IResponseHandlers WithObjectResult( 141 | this IResponseHandlers builder, Func valueFactory, int index = -1) 142 | where TException : Exception 143 | { 144 | return WithActionResult(builder, (request, exception) => new ObjectResult(valueFactory(request, exception)), index); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.NewtonsoftJson/Community.AspNetCore.ExceptionHandling.NewtonsoftJson.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Extension methods to configure exception handler which write object serialized using Newtonsoft.Json serializer to responce body. In case of using netcore2.1+ use Commmunity.AspNetCore.ExceptionHandling.Mvc package instead 6 | IharYakimush 7 | IharYakimush 8 | https://github.com/IharYakimush/asp-net-core-exception-handling/blob/develop/LICENSE 9 | https://github.com/IharYakimush/asp-net-core-exception-handling 10 | aspnetcore exception handling policy json response 11 | 2.2.0.0 12 | 2.2.0 13 | 14 | true 15 | true 16 | Fixed compatibility with AspNetCore 2.2 and AspNetCore 3.0 for log handler. Breaking changes in log handler configuration. 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.NewtonsoftJson/PolicyBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Community.AspNetCore.ExceptionHandling.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Net.Http.Headers; 9 | using Newtonsoft.Json; 10 | 11 | namespace Community.AspNetCore.ExceptionHandling.NewtonsoftJson 12 | { 13 | public static class PolicyBuilderExtensions 14 | { 15 | /// 16 | /// Write serialized object to response using and pass control to next handler. 17 | /// 18 | /// 19 | /// The exception type 20 | /// 21 | /// 22 | /// The result object type. 23 | /// 24 | /// 25 | /// The policy builder 26 | /// 27 | /// 28 | /// The result object factory. 29 | /// 30 | /// 31 | /// The JSON serializer settings . 32 | /// 33 | /// 34 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 35 | /// 36 | /// 37 | /// Policy builder 38 | /// 39 | [Obsolete("In case of using netcore2.1+ use Community.AspNetCore.ExceptionHandling.Mvc instead")] 40 | public static IResponseHandlers WithBodyJson( 41 | this IResponseHandlers builder, Func valueFactory, JsonSerializerSettings settings = null, int index = -1) 42 | where TException : Exception 43 | { 44 | return builder.WithBody((request, stream, exception) => 45 | { 46 | if (settings == null) 47 | { 48 | settings = request.HttpContext.RequestServices.GetService(); 49 | } 50 | 51 | if (settings == null) 52 | { 53 | settings = new JsonSerializerSettings(); 54 | } 55 | 56 | JsonSerializer jsonSerializer = JsonSerializer.Create(settings); 57 | 58 | var headers = request.HttpContext.Response.Headers; 59 | if (!headers.ContainsKey(HeaderNames.ContentType)) 60 | { 61 | headers[HeaderNames.ContentType] = "application/json"; 62 | } 63 | 64 | TObject val = valueFactory(request, exception); 65 | 66 | //return Task.CompletedTask; 67 | using (MemoryStream ms = new MemoryStream()) 68 | { 69 | using (StreamWriter sw = new StreamWriter(ms, Encoding.UTF8, 1024, true)) 70 | { 71 | jsonSerializer.Serialize(sw, val); 72 | } 73 | 74 | ms.Seek(0, SeekOrigin.Begin); 75 | byte[] array = ms.ToArray(); 76 | stream.WriteAsync(array, 0, array.Length); 77 | 78 | return Task.CompletedTask; 79 | } 80 | }); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Tests/Community.AspNetCore.ExceptionHandling.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Tests/Exc/ReThrowExceptionHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Community.AspNetCore.ExceptionHandling.Exc; 3 | using Community.AspNetCore.ExceptionHandling.Handlers; 4 | using Xunit; 5 | 6 | namespace Community.AspNetCore.ExceptionHandling.Tests.Exc 7 | { 8 | 9 | public class ReThrowExceptionHandlerTests 10 | { 11 | [Fact] 12 | public async Task AlwaysReThrowResult() 13 | { 14 | ReThrowExceptionHandler handler = new ReThrowExceptionHandler(); 15 | HandlerResult result = await handler.Handle(null, null); 16 | 17 | Assert.Equal(HandlerResult.ReThrow, result); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Tests/Handlers/MarkHandledHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Community.AspNetCore.ExceptionHandling.Handlers; 3 | using Xunit; 4 | 5 | namespace Community.AspNetCore.ExceptionHandling.Tests.Handlers 6 | { 7 | public class MarkHandledHandlerTests 8 | { 9 | [Fact] 10 | public async Task AlwaysHandledResult() 11 | { 12 | MarkHandledHandler handler = new MarkHandledHandler(); 13 | HandlerResult result = await handler.Handle(null, null); 14 | 15 | Assert.Equal(HandlerResult.Handled, result); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Tests/Handlers/NextChainHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Community.AspNetCore.ExceptionHandling.Handlers; 3 | using Xunit; 4 | 5 | namespace Community.AspNetCore.ExceptionHandling.Tests.Handlers 6 | { 7 | public class NextChainHandlerTests 8 | { 9 | [Fact] 10 | public async Task AlwaysNextChainResult() 11 | { 12 | NextChainHandler handler = new NextChainHandler(); 13 | HandlerResult result = await handler.Handle(null, null); 14 | 15 | Assert.Equal(HandlerResult.NextChain, result); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Tests/Scenarious/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Community.AspNetCore.ExceptionHandling.Tests.Scenarious 5 | { 6 | [Serializable] 7 | public class BaseException : Exception 8 | { 9 | 10 | } 11 | 12 | [Serializable] 13 | public class NotBaseException : Exception 14 | { 15 | 16 | } 17 | 18 | [Serializable] 19 | public class RethrowException : BaseException 20 | { 21 | 22 | } 23 | 24 | [Serializable] 25 | public class NextEmptyException : BaseException 26 | { 27 | 28 | } 29 | 30 | [Serializable] 31 | public class EmptyChainException : BaseException 32 | { 33 | 34 | } 35 | 36 | [Serializable] 37 | public class HandledWithoutResponseException : BaseException 38 | { 39 | 40 | } 41 | 42 | [Serializable] 43 | public class CommonResponseException : BaseException 44 | { 45 | 46 | } 47 | 48 | [Serializable] 49 | public class CustomResponseException : BaseException 50 | { 51 | 52 | } 53 | 54 | [Serializable] 55 | public class CustomObjectResponseException : BaseException 56 | { 57 | 58 | } 59 | 60 | [Serializable] 61 | public class CustomJsonResponseException : BaseException 62 | { 63 | 64 | } 65 | 66 | [Serializable] 67 | public class RetryException : BaseException 68 | { 69 | 70 | } 71 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Tests/Scenarious/ScenariosTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | using Community.AspNetCore.ExceptionHandling.Tests.Scenarious; 9 | using Microsoft.AspNetCore; 10 | using Xunit; 11 | using Microsoft.AspNetCore.Hosting; 12 | 13 | namespace Community.AspNetCore.ExceptionHandling.Tests 14 | { 15 | public class ScenariosTests : WebApplicationFactory 16 | { 17 | protected override IWebHostBuilder CreateWebHostBuilder() 18 | { 19 | return WebHost.CreateDefaultBuilder().UseStartup(); 20 | } 21 | 22 | private HttpClient Client => 23 | this.CreateDefaultClient(new Uri("http://example.com")); 24 | 25 | [Fact] 26 | public async Task Ok() 27 | { 28 | HttpResponseMessage resp = await Client.GetAsync("ok"); 29 | await AssertResponse(resp, HttpStatusCode.OK, "ok"); 30 | } 31 | 32 | [Fact] 33 | public async Task HandledWithoutResponse() 34 | { 35 | HttpResponseMessage resp = await Client.GetAsync("handled"); 36 | var str = await AssertResponse(resp, HttpStatusCode.OK, null); 37 | Assert.Equal(string.Empty, str); 38 | } 39 | 40 | [Fact] 41 | public async Task CustomResponse() 42 | { 43 | HttpResponseMessage resp = await Client.GetAsync("custom"); 44 | await AssertResponse(resp, HttpStatusCode.BadRequest, "customResponse", 45 | new KeyValuePair("X-Custom", "val")); 46 | } 47 | 48 | [Fact] 49 | public async Task CustomObjectResponse() 50 | { 51 | HttpResponseMessage resp = await Client.GetAsync("object"); 52 | await AssertResponse(resp, HttpStatusCode.NotFound, "message"); 53 | } 54 | 55 | [Fact] 56 | public async Task CustomJsonResponse() 57 | { 58 | HttpResponseMessage resp = await Client.GetAsync("json"); 59 | await AssertResponse(resp, HttpStatusCode.Forbidden, "message"); 60 | } 61 | 62 | [Fact] 63 | public async Task CommonResponse() 64 | { 65 | HttpResponseMessage resp = await Client.GetAsync("common"); 66 | await AssertResponse(resp, HttpStatusCode.InternalServerError, "commonResponse", 67 | new KeyValuePair("X-Common", "val")); 68 | } 69 | 70 | [Fact] 71 | public async Task ReThrow() 72 | { 73 | HttpResponseMessage resp = await Client.GetAsync("rethrow"); 74 | await AssertUnhandledException(resp); 75 | } 76 | 77 | [Fact] 78 | public async Task NextHandlerNotAwailable() 79 | { 80 | HttpResponseMessage resp = await Client.GetAsync("nextempty"); 81 | await AssertUnhandledException(resp); 82 | } 83 | 84 | [Fact] 85 | public async Task NextPolicyNotAwailable() 86 | { 87 | HttpResponseMessage resp = await Client.GetAsync("nextpolicyempty"); 88 | await AssertUnhandledException(resp); 89 | } 90 | 91 | [Fact] 92 | public async Task EmptyChain() 93 | { 94 | HttpResponseMessage resp = await Client.GetAsync("emptychain"); 95 | await AssertUnhandledException(resp); 96 | } 97 | 98 | private static async Task AssertUnhandledException(HttpResponseMessage response) 99 | { 100 | Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); 101 | string str = await response.Content.ReadAsStringAsync(); 102 | Assert.Contains("An unhandled exception occurred while processing the request", str); 103 | Assert.Contains(typeof(TException).Name, str); 104 | } 105 | 106 | private static async Task AssertResponse(HttpResponseMessage response, HttpStatusCode statusCode, string body, params KeyValuePair[] headers) 107 | { 108 | Assert.Equal(statusCode, response.StatusCode); 109 | string str = await response.Content.ReadAsStringAsync(); 110 | foreach (var header in headers) 111 | { 112 | Assert.True(response.Headers.Contains(header.Key), $"Header {header.Key} not awailable"); 113 | Assert.Equal(header.Value, response.Headers.GetValues(header.Key).First()); 114 | } 115 | 116 | if (body != null) 117 | { 118 | Assert.Contains(body, str); 119 | } 120 | 121 | return str; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling.Tests/Scenarious/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text; 4 | using Community.AspNetCore.ExceptionHandling.Mvc; 5 | using Community.AspNetCore.ExceptionHandling.NewtonsoftJson; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Community.AspNetCore.ExceptionHandling.Tests.Scenarious 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | services.AddMvc(); 28 | 29 | services.AddExceptionHandlingPolicies(options => 30 | { 31 | options.For().Rethrow(); 32 | 33 | options.For().Log(); 34 | 35 | options.For().Handled(); 36 | 37 | options.For(); 38 | 39 | options.For().Log().NextPolicy(); 40 | 41 | options.For().Response(e => 400).Headers((h, e) => h.Add("X-Custom", "val")) 42 | .WithBody(async (r, w, e) => 43 | { 44 | //await w.BaseStream.WriteAsync(Encoding.UTF8.GetBytes("customResponse")); 45 | byte[] array = Encoding.UTF8.GetBytes("customResponse"); 46 | await w.WriteAsync(array, 0, array.Length); 47 | //await w.FlushAsync(); 48 | }) 49 | .Handled(); 50 | 51 | options.For().Response(e => 404) 52 | .WithObjectResult(new {message="qwe"}) 53 | .Handled(); 54 | 55 | options.For().Response(e => 403) 56 | .WithBodyJson((r, e) => new { message = "qwe" }) 57 | .Handled(); 58 | 59 | options.For().Log().NextPolicy(); 60 | 61 | options.For() 62 | .Log(lo => { lo.LogAction = (l, c, e) => l.LogError(e, "qwe"); }) 63 | .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler) 64 | .ClearCacheHeaders() 65 | .Headers((h, e) => h.Add("X-Common", "val")) 66 | .WithBody(async(r, w, e) => 67 | { 68 | byte[] array = Encoding.UTF8.GetBytes(e.GetType().Name + "_commonResponse"); 69 | await w.WriteAsync(array, 0, array.Length); 70 | }) 71 | .Handled(); 72 | }); 73 | 74 | services.AddLogging(); 75 | } 76 | 77 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 78 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 79 | { 80 | //app.UseResponseBuffering(); 81 | app.UseDeveloperExceptionPage(); 82 | app.UseExceptionHandlingPolicies().Use(async (context, func) => 83 | { 84 | if (context.Request.Path.StartsWithSegments(new PathString("/rethrow"))) 85 | { 86 | throw new RethrowException(); 87 | } 88 | 89 | if (context.Request.Path.StartsWithSegments(new PathString("/nextempty"))) 90 | { 91 | throw new NextEmptyException(); 92 | } 93 | 94 | if (context.Request.Path.StartsWithSegments(new PathString("/nextpolicyempty"))) 95 | { 96 | throw new NotBaseException(); 97 | } 98 | 99 | if (context.Request.Path.StartsWithSegments(new PathString("/emptychain"))) 100 | { 101 | throw new EmptyChainException(); 102 | } 103 | 104 | if (context.Request.Path.StartsWithSegments(new PathString("/handled"))) 105 | { 106 | throw new HandledWithoutResponseException(); 107 | } 108 | 109 | if (context.Request.Path.StartsWithSegments(new PathString("/custom"))) 110 | { 111 | throw new CustomResponseException(); 112 | } 113 | 114 | if (context.Request.Path.StartsWithSegments(new PathString("/common"))) 115 | { 116 | throw new CommonResponseException(); 117 | } 118 | 119 | if (context.Request.Path.StartsWithSegments(new PathString("/object"))) 120 | { 121 | throw new CustomObjectResponseException(); 122 | } 123 | 124 | if (context.Request.Path.StartsWithSegments(new PathString("/json"))) 125 | { 126 | throw new CustomJsonResponseException(); 127 | } 128 | 129 | context.Response.StatusCode = 200; 130 | await context.Response.Body.WriteAsync(Encoding.UTF8.GetBytes("ok")); 131 | }); 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/AppBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Community.AspNetCore.ExceptionHandling.Builder; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Community.AspNetCore.ExceptionHandling 9 | { 10 | //TODO: add warning for policy override 11 | //TODO: add response handler 12 | //TODO: add retry handler 13 | //TODO: policy builder 14 | //TODO: add api exception and handler 15 | //TODO: add terminate policies pipeline handler ??? 16 | public static class AppBuilderExtensions 17 | { 18 | public static IApplicationBuilder UseExceptionHandlingPolicies(this IApplicationBuilder app) 19 | { 20 | return app.UseMiddleware(); 21 | } 22 | public static IServiceCollection AddExceptionHandlingPolicies(this IServiceCollection services, Action builder) 23 | { 24 | PolicyBuilder policyBuilder = new PolicyBuilder(services); 25 | builder?.Invoke(policyBuilder); 26 | services.TryAddSingleton>(policyBuilder.Options); 27 | services.TryAddSingleton(); 28 | 29 | return policyBuilder; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Builder/ExceptionMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Community.AspNetCore.ExceptionHandling.Builder 5 | { 6 | public class ExceptionMapping : IResponseHandlers 7 | where TException : Exception 8 | { 9 | public IExceptionPolicyBuilder Builder { get; } 10 | 11 | internal ExceptionMapping(IExceptionPolicyBuilder builder) 12 | { 13 | Builder = builder ?? throw new ArgumentNullException(nameof(builder)); 14 | } 15 | 16 | public IServiceCollection Services => Builder.Services; 17 | 18 | public ExceptionHandlingPolicyOptions Options => Builder.Options; 19 | } 20 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Builder/IExceptionMapping.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Community.AspNetCore.ExceptionHandling.Builder 4 | { 5 | public interface IExceptionMapping : IExceptionPolicyBuilder 6 | where TException : Exception 7 | { 8 | } 9 | 10 | public interface IResponseHandlers : IExceptionMapping 11 | where TException : Exception 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Builder/PolicyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Community.AspNetCore.ExceptionHandling.Builder 7 | { 8 | public class PolicyBuilder : IExceptionPolicyBuilder, IServiceCollection 9 | { 10 | public IServiceCollection Services { get; } 11 | 12 | public ExceptionHandlingPolicyOptions Options { get; } = new ExceptionHandlingPolicyOptions(); 13 | 14 | public PolicyBuilder(IServiceCollection services) 15 | { 16 | this.Services = services ?? throw new ArgumentNullException(nameof(services)); 17 | } 18 | 19 | public IEnumerator GetEnumerator() 20 | { 21 | return Services.GetEnumerator(); 22 | } 23 | 24 | IEnumerator IEnumerable.GetEnumerator() 25 | { 26 | return ((IEnumerable) Services).GetEnumerator(); 27 | } 28 | 29 | public void Add(ServiceDescriptor item) 30 | { 31 | Services.Add(item); 32 | } 33 | 34 | public void Clear() 35 | { 36 | Services.Clear(); 37 | } 38 | 39 | public bool Contains(ServiceDescriptor item) 40 | { 41 | return Services.Contains(item); 42 | } 43 | 44 | public void CopyTo(ServiceDescriptor[] array, int arrayIndex) 45 | { 46 | Services.CopyTo(array, arrayIndex); 47 | } 48 | 49 | public bool Remove(ServiceDescriptor item) 50 | { 51 | return Services.Remove(item); 52 | } 53 | 54 | public int Count => Services.Count; 55 | 56 | public bool IsReadOnly => Services.IsReadOnly; 57 | 58 | public int IndexOf(ServiceDescriptor item) 59 | { 60 | return Services.IndexOf(item); 61 | } 62 | 63 | public void Insert(int index, ServiceDescriptor item) 64 | { 65 | Services.Insert(index, item); 66 | } 67 | 68 | public void RemoveAt(int index) 69 | { 70 | Services.RemoveAt(index); 71 | } 72 | 73 | public ServiceDescriptor this[int index] 74 | { 75 | get => Services[index]; 76 | set => Services[index] = value; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Community.AspNetCore.ExceptionHandling.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Middleware to configure exception handling policies. Configure chain of handlers per exception type. OOTB handlers: log, retry, set responce headers and body 6 | https://github.com/IharYakimush/asp-net-core-exception-handling 7 | https://github.com/IharYakimush/asp-net-core-exception-handling/blob/develop/LICENSE 8 | IharYakimush 9 | aspnetcore exception handling policy 10 | 2.2.0.0 11 | true 12 | true 13 | 2.2.0.0 14 | 15 | IharYakimush 16 | 2.2.0 17 | true 18 | ..\sgn.snk 19 | 20 | Library 21 | 22 | Fixed compatibility with AspNetCore 2.2 and AspNetCore 3.0 for log handler. Breaking changes in log handler configuration. 23 | 24 | 25 | 26 | TRACE;DEBUG 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Const.cs: -------------------------------------------------------------------------------- 1 | namespace Community.AspNetCore.ExceptionHandling 2 | { 3 | public static class Const 4 | { 5 | public const string Category = "Community.AspNetCore.ExceptionHandling"; 6 | } 7 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Events.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | 3 | namespace Community.AspNetCore.ExceptionHandling 4 | { 5 | public static class Events 6 | { 7 | public static readonly EventId HandlerError = new EventId(100, "HandlerExecutionError"); 8 | public static readonly EventId PolicyNotFound = new EventId(101, "PolicyForExceptionNotRegistered"); 9 | public static readonly EventId HandlersNotFound = new EventId(102, "HandlersCollectionEmpty"); 10 | public static readonly EventId HandlerNotCreated = new EventId(103, "HandlersCanNotBeCreated"); 11 | public static readonly EventId RetryForStartedResponce = new EventId(104, "RetryForStartedResponce"); 12 | public static readonly EventId Retry = new EventId(105, "Retry"); 13 | public static readonly EventId UnhandledResult = new EventId(106, "UnhandledResult"); 14 | public static readonly EventId RetryIterationExceedLimit = new EventId(107, "RetryIterationExceedLimit"); 15 | } 16 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Exc/ReThrowExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Community.AspNetCore.ExceptionHandling.Handlers; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Community.AspNetCore.ExceptionHandling.Exc 7 | { 8 | public class ReThrowExceptionHandler : IExceptionHandler 9 | { 10 | public Task Handle(HttpContext httpContext, Exception exception) 11 | { 12 | return Task.FromResult(HandlerResult.ReThrow); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/ExceptionHandlingPolicyMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Community.AspNetCore.ExceptionHandling.Handlers; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Logging.Abstractions; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace Community.AspNetCore.ExceptionHandling 12 | { 13 | public class ExceptionHandlingPolicyMiddleware : IMiddleware 14 | { 15 | public const int MaxRetryIterations = 10; 16 | 17 | private readonly IOptions options; 18 | 19 | public ExceptionHandlingPolicyMiddleware(IOptions options) 20 | { 21 | this.options = options ?? throw new ArgumentNullException(nameof(options)); 22 | } 23 | 24 | private static async Task EnumerateExceptionMapping( 25 | HttpContext context, 26 | ExceptionHandlingPolicyOptions policyOptions, 27 | Exception exception, 28 | ILogger logger) 29 | { 30 | Type exceptionType = exception.GetType(); 31 | 32 | bool mappingExists = false; 33 | HandlerResult handleResult = HandlerResult.ReThrow; 34 | 35 | foreach (Type type in policyOptions.GetExceptionsInternal()) 36 | { 37 | if (type.IsAssignableFrom(exceptionType)) 38 | { 39 | mappingExists = true; 40 | handleResult = await EnumerateHandlers(context, type, exception, policyOptions, logger); 41 | 42 | if (handleResult != HandlerResult.NextChain) 43 | { 44 | break; 45 | } 46 | } 47 | } 48 | 49 | if (!mappingExists) 50 | { 51 | logger.LogWarning(Events.PolicyNotFound, 52 | "Handlers mapping for exception type {exceptionType} not exists. Exception will be re-thrown. RequestId: {RequestId}", 53 | exceptionType, context.TraceIdentifier); 54 | 55 | return HandlerResult.ReThrow; 56 | } 57 | 58 | return handleResult; 59 | } 60 | 61 | private static async Task EnumerateHandlers( 62 | HttpContext context, 63 | Type exceptionType, 64 | Exception exception, 65 | ExceptionHandlingPolicyOptions policyOptions, 66 | ILogger logger) 67 | { 68 | bool handlerExecuted = false; 69 | HandlerResult handleResult = HandlerResult.ReThrow; 70 | 71 | IEnumerable handlers = policyOptions.GetHandlersInternal(exceptionType); 72 | 73 | foreach (Type handlerType in handlers) 74 | { 75 | try 76 | { 77 | IExceptionHandler handler = 78 | context.RequestServices.GetService(handlerType) as IExceptionHandler; 79 | 80 | if (handler == null) 81 | { 82 | handlerExecuted = false; 83 | logger.LogError(Events.HandlerNotCreated, 84 | "Handler type {handlerType} can't be created because it not registered in IServiceProvider. RequestId: {RequestId}", 85 | handlerType, context.TraceIdentifier); 86 | } 87 | else 88 | { 89 | handleResult = await handler.Handle(context, exception); 90 | handlerExecuted = true; 91 | } 92 | } 93 | catch (Exception e) 94 | { 95 | logger.LogError(Events.HandlerError, e, 96 | "Unhandled exception executing handler of type {handlerType} on exception of type {exceptionType}. RequestId: {RequestId}", 97 | handlerType, exceptionType, context.TraceIdentifier); 98 | 99 | return HandlerResult.ReThrow; 100 | } 101 | 102 | if (handleResult != HandlerResult.NextHandler) 103 | { 104 | break; 105 | } 106 | } 107 | 108 | if (!handlerExecuted) 109 | { 110 | logger.LogWarning(Events.HandlersNotFound, 111 | "Handlers collection for exception type {exceptionType} is empty. Exception will be re-thrown. RequestId: {RequestId}", 112 | exceptionType, context.TraceIdentifier); 113 | 114 | return HandlerResult.ReThrow; 115 | } 116 | 117 | return handleResult; 118 | } 119 | 120 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 121 | { 122 | ILogger logger = context.RequestServices.GetService>() ?? 123 | NullLoggerFactory.Instance.CreateLogger(Const.Category); 124 | 125 | await InvokeWithRetryAsync(context, next, logger, 0); 126 | } 127 | 128 | private async Task InvokeWithRetryAsync(HttpContext context, RequestDelegate next, ILogger logger, int iteration) 129 | { 130 | try 131 | { 132 | await next(context); 133 | } 134 | catch (Exception exception) 135 | { 136 | ExceptionHandlingPolicyOptions policyOptions = this.options.Value; 137 | 138 | var result = await EnumerateExceptionMapping(context, policyOptions, exception, logger); 139 | 140 | if (result == HandlerResult.ReThrow) 141 | { 142 | throw; 143 | } 144 | 145 | if (result == HandlerResult.Retry) 146 | { 147 | // We can't do anything if the response has already started, just abort. 148 | if (context.Response.HasStarted) 149 | { 150 | logger.LogWarning(Events.RetryForStartedResponce, 151 | exception, 152 | "Retry requested when response already started. Exception will be re-thrown. RequestId: {RequestId}", 153 | context.TraceIdentifier); 154 | 155 | throw; 156 | } 157 | 158 | if (iteration > MaxRetryIterations) 159 | { 160 | logger.LogCritical(Events.RetryIterationExceedLimit, 161 | exception, 162 | "Retry iterations count exceed limit of {limit}. Possible issues with retry policy configuration. Exception will be re-thrown. RequestId: {RequestId}", 163 | MaxRetryIterations, 164 | context.TraceIdentifier); 165 | 166 | throw; 167 | } 168 | 169 | logger.LogWarning(Events.Retry, 170 | exception, 171 | "Retry requested. Iteration {iteration} RequestId: {RequestId}", 172 | iteration, 173 | context.TraceIdentifier); 174 | 175 | context.Response.Headers.Clear(); 176 | 177 | 178 | await InvokeWithRetryAsync(context, next, logger, iteration + 1); 179 | } 180 | 181 | if (result != HandlerResult.Handled) 182 | { 183 | logger.LogWarning(Events.UnhandledResult, 184 | exception, 185 | "After execution of all handlers exception was not marked as handled and will be re thrown. RequestId: {RequestId}", 186 | context.TraceIdentifier); 187 | 188 | throw; 189 | } 190 | } 191 | } 192 | } 193 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/ExceptionHandlingPolicyOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace Community.AspNetCore.ExceptionHandling 8 | { 9 | public class ExceptionHandlingPolicyOptions : IOptions 10 | { 11 | public ExceptionHandlingPolicyOptions Value => this; 12 | 13 | private readonly OrderedDictionary handlers = new OrderedDictionary(); 14 | 15 | public ExceptionHandlingPolicyOptions EnsureException(Type exceptionType, int index = -1) 16 | { 17 | if (!typeof(Exception).IsAssignableFrom(exceptionType)) 18 | { 19 | throw new ArgumentOutOfRangeException(nameof(exceptionType), exceptionType, 20 | $"Exception type should implement {typeof(Exception).Name}"); 21 | } 22 | 23 | if (handlers.Contains(exceptionType)) 24 | { 25 | if (index >= 0) 26 | { 27 | object values = handlers[exceptionType]; 28 | handlers.Remove(exceptionType); 29 | handlers.Insert(index, exceptionType, values); 30 | } 31 | } 32 | else 33 | { 34 | if (index < 0) 35 | { 36 | handlers.Add(exceptionType, new List()); 37 | } 38 | else 39 | { 40 | handlers.Insert(index, exceptionType, new List()); 41 | } 42 | } 43 | 44 | return this; 45 | } 46 | 47 | public ExceptionHandlingPolicyOptions RemoveException(Type exceptionType) 48 | { 49 | if (this.handlers.Contains(exceptionType)) 50 | { 51 | this.handlers.Remove(exceptionType); 52 | } 53 | 54 | return this; 55 | } 56 | 57 | public ExceptionHandlingPolicyOptions EnsureHandler(Type exceptionType, Type handlerType, int index = -1) 58 | { 59 | if (!typeof(IExceptionHandler).IsAssignableFrom(handlerType)) 60 | { 61 | throw new ArgumentOutOfRangeException(nameof(handlerType), handlerType, 62 | $"Handler type should implement {typeof(IExceptionHandler).Name}"); 63 | } 64 | 65 | this.EnsureException(exceptionType); 66 | 67 | List list = this.handlers[exceptionType] as List; 68 | 69 | ProcessHandlersList(list, handlerType, index); 70 | 71 | return this; 72 | } 73 | 74 | private static void ProcessHandlersList(List list, Type handlerType, int index) 75 | { 76 | if (list.Any(type => type == handlerType)) 77 | { 78 | if (index >= 0) 79 | { 80 | list.Remove(handlerType); 81 | list.Insert(index, handlerType); 82 | } 83 | } 84 | else 85 | { 86 | if (index < 0) 87 | { 88 | list.Add(handlerType); 89 | } 90 | else 91 | { 92 | list.Insert(index, handlerType); 93 | } 94 | } 95 | } 96 | 97 | public ExceptionHandlingPolicyOptions RemoveHandler(Type exceptionType, Type handlerType) 98 | { 99 | if (this.handlers.Contains(exceptionType)) 100 | { 101 | List list = this.handlers[exceptionType] as List; 102 | 103 | if (list.Contains(handlerType)) 104 | { 105 | list.Remove(handlerType); 106 | } 107 | } 108 | 109 | return this; 110 | } 111 | 112 | public ExceptionHandlingPolicyOptions ClearExceptions() 113 | { 114 | this.handlers.Clear(); 115 | return this; 116 | } 117 | 118 | public ExceptionHandlingPolicyOptions ClearHandlers(Type exceptionType) 119 | { 120 | if (this.handlers.Contains(exceptionType)) 121 | { 122 | List list = this.handlers[exceptionType] as List; 123 | 124 | list.Clear(); 125 | } 126 | 127 | return this; 128 | } 129 | 130 | internal IEnumerable GetHandlersInternal(Type exceptionType) 131 | { 132 | if (this.handlers.Contains(exceptionType)) 133 | { 134 | return this.handlers[exceptionType] as List; 135 | } 136 | 137 | return Enumerable.Empty(); 138 | } 139 | 140 | internal IEnumerable GetExceptionsInternal() 141 | { 142 | return this.handlers.Keys.OfType(); 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Handlers/HandlerResult.cs: -------------------------------------------------------------------------------- 1 | namespace Community.AspNetCore.ExceptionHandling.Handlers 2 | { 3 | public enum HandlerResult 4 | { 5 | ReThrow = 0, 6 | 7 | NextHandler = 1, 8 | 9 | NextChain = 2, 10 | 11 | Retry = 3, 12 | 13 | Handled = 4 14 | } 15 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Handlers/HandlerStrongType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Community.AspNetCore.ExceptionHandling.Handlers 7 | { 8 | public abstract class HandlerStrongType : HandlerWithLogger, IExceptionHandler 9 | where TException : Exception 10 | { 11 | private static readonly EventId ExceptionTypeNotMatchGenericType = new EventId(136, "ExceptionTypeNotMatchGenericType"); 12 | 13 | protected HandlerStrongType(HandlerWithLoggerOptions options, ILoggerFactory loggerFactory) : base(options, 14 | loggerFactory) 15 | { 16 | } 17 | 18 | public async Task Handle(HttpContext httpContext, Exception exception) 19 | { 20 | if (exception is TException e) 21 | { 22 | return await this.HandleStrongType(httpContext, e); 23 | } 24 | else 25 | { 26 | this.Logger.LogError(ExceptionTypeNotMatchGenericType, 27 | "Excpetion type {exceptionType} not match current generic type {genericType}. Exception will be re-thrown.", 28 | exception.GetType(), typeof(TException)); 29 | 30 | return HandlerResult.ReThrow; 31 | } 32 | } 33 | 34 | protected abstract Task HandleStrongType(HttpContext httpContext, TException exception); 35 | } 36 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Handlers/HandlerWithLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Community.AspNetCore.ExceptionHandling.Handlers 5 | { 6 | public class HandlerWithLogger 7 | { 8 | private readonly HandlerWithLoggerOptions _options; 9 | private readonly ILoggerFactory _loggerFactory; 10 | 11 | public HandlerWithLogger(HandlerWithLoggerOptions options, ILoggerFactory loggerFactory) 12 | { 13 | _options = options ?? throw new ArgumentNullException(nameof(options)); 14 | _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); 15 | } 16 | 17 | protected ILogger Logger => this._loggerFactory.CreateLogger(_options.LoggerCategory); 18 | } 19 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Handlers/HandlerWithLoggerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Community.AspNetCore.ExceptionHandling.Handlers 2 | { 3 | public class HandlerWithLoggerOptions 4 | { 5 | private string _loggerCategory = null; 6 | 7 | public string LoggerCategory 8 | { 9 | get => _loggerCategory ?? Const.Category; 10 | set 11 | { 12 | if (!string.IsNullOrWhiteSpace(value)) 13 | { 14 | this._loggerCategory = value; 15 | } 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Handlers/MarkHandledHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Community.AspNetCore.ExceptionHandling.Handlers 6 | { 7 | public class MarkHandledHandler : IExceptionHandler 8 | { 9 | public Task Handle(HttpContext httpContext, Exception exception) 10 | { 11 | return Task.FromResult(HandlerResult.Handled); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Handlers/NextChainHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Community.AspNetCore.ExceptionHandling.Handlers 6 | { 7 | public class NextChainHandler : IExceptionHandler 8 | { 9 | public Task Handle(HttpContext httpContext, Exception exception) 10 | { 11 | return Task.FromResult(HandlerResult.NextChain); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/IExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Community.AspNetCore.ExceptionHandling.Handlers; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Community.AspNetCore.ExceptionHandling 7 | { 8 | public interface IExceptionHandler 9 | { 10 | Task Handle(HttpContext httpContext, Exception exception); 11 | } 12 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/IExceptionPolicyBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Community.AspNetCore.ExceptionHandling 4 | { 5 | public interface IExceptionPolicyBuilder 6 | { 7 | IServiceCollection Services { get; } 8 | 9 | ExceptionHandlingPolicyOptions Options { get; } 10 | } 11 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Logs/DisableLoggingHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Community.AspNetCore.ExceptionHandling.Handlers; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Community.AspNetCore.ExceptionHandling.Logs 7 | { 8 | class DisableLoggingHandler : IExceptionHandler 9 | { 10 | public const string DisableLoggingFlagKey = "427EDE68BE9A"; 11 | 12 | public Task Handle(HttpContext httpContext, Exception exception) 13 | { 14 | exception.Data[DisableLoggingFlagKey] = string.Empty; 15 | 16 | return Task.FromResult(HandlerResult.NextHandler); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Logs/LogExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Community.AspNetCore.ExceptionHandling.Handlers; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Logging.Internal; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Community.AspNetCore.ExceptionHandling.Logs 10 | { 11 | public class LogExceptionHandler : HandlerStrongType 12 | where TException : Exception 13 | { 14 | private readonly IOptions> _settings; 15 | 16 | private static readonly EventId DefaultEvent = new EventId(500, "UnhandledException"); 17 | 18 | private static readonly EventId LogActionErrorEvent = new EventId(501, "LogActionError"); 19 | 20 | public LogHandlerOptions Settings => this._settings.Value; 21 | 22 | public LogExceptionHandler(IOptions> settings, ILoggerFactory loggerFactory):base(settings.Value, loggerFactory) 23 | { 24 | _settings = settings ?? throw new ArgumentNullException(nameof(settings)); 25 | } 26 | 27 | protected override Task HandleStrongType(HttpContext httpContext, TException exception) 28 | { 29 | if (exception.Data.Contains(DisableLoggingHandler.DisableLoggingFlagKey)) 30 | { 31 | return Task.FromResult(HandlerResult.NextHandler); 32 | } 33 | 34 | if (httpContext.RequestServices.GetService(typeof(ILoggerFactory)) is ILoggerFactory loggerFactory) 35 | { 36 | ILogger logger = 37 | loggerFactory.CreateLogger(this.Settings.Category?.Invoke(httpContext, exception) ?? 38 | Const.Category); 39 | 40 | Action logAction = this.Settings.LogAction ?? LogDefault; 41 | 42 | try 43 | { 44 | logAction(logger, httpContext, exception); 45 | } 46 | catch (Exception logException) 47 | { 48 | try 49 | { 50 | logger.LogWarning(DefaultEvent, logException, "Unhandled error occured in log action."); 51 | } 52 | catch 53 | { 54 | // don't fail in case of errors with this log 55 | } 56 | 57 | if (this.Settings.RethrowLogActionExceptions) 58 | { 59 | throw; 60 | } 61 | } 62 | } 63 | 64 | return Task.FromResult(HandlerResult.NextHandler); 65 | } 66 | 67 | private static void LogDefault(ILogger logger, HttpContext context, TException exception) 68 | { 69 | logger.LogError(DefaultEvent, exception, "Unhandled error occured. RequestId: {requestId}.", context.TraceIdentifier); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Logs/LogHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Community.AspNetCore.ExceptionHandling.Handlers; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Internal; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Community.AspNetCore.ExceptionHandling.Logs 9 | { 10 | /// 11 | /// The log handler options 12 | /// 13 | /// 14 | /// The exception type 15 | /// 16 | public class LogHandlerOptions : HandlerWithLoggerOptions, IOptions> 17 | where TException : Exception 18 | { 19 | public LogHandlerOptions Value => this; 20 | 21 | /// 22 | /// Action to log exception. If not set logger.LogError("Unhandled error occured. RequestId: {requestId}.", httpContext.TraceIdentifier); will be used by default. 23 | /// 24 | public Action LogAction { get; set; } 25 | 26 | /// 27 | /// Factory for log category. By default "Community.AspNetCore.ExceptionHandling" will be used. 28 | /// 29 | public Func Category { get; set; } 30 | 31 | /// 32 | /// Rethrow exception from LogAction. 33 | /// 34 | public bool RethrowLogActionExceptions { get; set; } = false; 35 | } 36 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/PolicyBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Community.AspNetCore.ExceptionHandling.Builder; 5 | using Community.AspNetCore.ExceptionHandling.Exc; 6 | using Community.AspNetCore.ExceptionHandling.Handlers; 7 | using Community.AspNetCore.ExceptionHandling.Logs; 8 | using Community.AspNetCore.ExceptionHandling.Response; 9 | using Community.AspNetCore.ExceptionHandling.Retry; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.DependencyInjection.Extensions; 13 | using Microsoft.Net.Http.Headers; 14 | 15 | namespace Community.AspNetCore.ExceptionHandling 16 | { 17 | public static class PolicyBuilderExtensions 18 | { 19 | public static IExceptionMapping For( 20 | this IExceptionPolicyBuilder builder, int index = -1) where TException : Exception 21 | { 22 | builder.Options.EnsureException(typeof(TException), index); 23 | return new ExceptionMapping(builder); 24 | } 25 | 26 | public static void EnsureHandler( 27 | this IExceptionPolicyBuilder builder, int index = -1) 28 | where THandler : class , IExceptionHandler 29 | where TException : Exception 30 | 31 | { 32 | builder.Options.Value.EnsureHandler(typeof(TException), typeof(THandler), index); 33 | builder.Services.TryAddSingleton(); 34 | } 35 | 36 | public static IExceptionMapping RemoveHandler( 37 | this IExceptionMapping builder) 38 | where THandler : IExceptionHandler 39 | where TException : Exception 40 | { 41 | builder.Options.Value.RemoveHandler(typeof(TException), typeof(THandler)); 42 | return builder; 43 | } 44 | 45 | public static IExceptionMapping Clear( 46 | this IExceptionMapping builder) 47 | where TException : Exception 48 | { 49 | builder.Options.Value.ClearHandlers(typeof(TException)); 50 | return builder; 51 | } 52 | 53 | // rethrow 54 | /// 55 | /// Re throw exception and stop handlers chain processing. 56 | /// 57 | /// 58 | /// The exception type 59 | /// 60 | /// 61 | /// The policy builder 62 | /// 63 | /// 64 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 65 | /// 66 | /// 67 | /// Policy builder 68 | /// 69 | public static IExceptionPolicyBuilder Rethrow( 70 | this IExceptionMapping builder, int index = -1) 71 | where TException : Exception 72 | { 73 | builder.EnsureHandler(index); 74 | return builder; 75 | } 76 | 77 | // next chain 78 | /// 79 | /// Terminates current handlers chain and try to execute next handlers chain which match exception type. 80 | /// 81 | /// 82 | /// The exception type 83 | /// 84 | /// 85 | /// The policy builder 86 | /// 87 | /// 88 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 89 | /// 90 | /// 91 | /// Policy builder 92 | /// 93 | public static IExceptionPolicyBuilder NextPolicy( 94 | this IExceptionMapping builder, int index = -1) 95 | where TException : Exception 96 | { 97 | builder.EnsureHandler(index); 98 | return builder; 99 | } 100 | 101 | // mark handled 102 | /// 103 | /// Terminate handlers chain execution and mark exception as "handled" which means that it will not be re thrown. 104 | /// 105 | /// 106 | /// The exception type 107 | /// 108 | /// 109 | /// The policy builder 110 | /// 111 | /// 112 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 113 | /// 114 | /// 115 | /// Policy builder 116 | /// 117 | public static IExceptionPolicyBuilder Handled( 118 | this IExceptionMapping builder, int index = -1) 119 | where TException : Exception 120 | { 121 | builder.EnsureHandler(index); 122 | return builder; 123 | } 124 | 125 | // Retry 126 | /// 127 | /// Terminate handlers chain and execute middleware pipeline (registered after exception handling policy middleware) again. 128 | /// 129 | /// 130 | /// The exception type 131 | /// 132 | /// 133 | /// The policy builder 134 | /// 135 | /// 136 | /// The retry settings. See for details. 137 | /// 138 | /// 139 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 140 | /// 141 | /// 142 | /// Policy builder 143 | /// 144 | public static IExceptionMapping Retry( 145 | this IExceptionMapping builder, Action> settings = null, int index = -1) 146 | where TException : Exception 147 | { 148 | builder.Services.Configure>(opt => settings?.Invoke(opt)); 149 | 150 | builder.EnsureHandler>(index); 151 | 152 | return builder; 153 | } 154 | 155 | // Log 156 | /// 157 | /// Log exception using registered in services collection and pass control to next handler in chain 158 | /// 159 | /// 160 | /// The exception type 161 | /// 162 | /// 163 | /// The policy builder 164 | /// 165 | /// 166 | /// The logs settings. See for details. 167 | /// 168 | /// 169 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 170 | /// 171 | /// 172 | /// Policy builder 173 | /// 174 | public static IExceptionMapping Log( 175 | this IExceptionMapping builder, Action> settings = null, int index = -1) 176 | where TException : Exception 177 | { 178 | builder.Services.Configure>(opt => settings?.Invoke(opt)); 179 | 180 | builder.EnsureHandler>(index); 181 | 182 | return builder; 183 | } 184 | /// 185 | /// Disable logging of this particular exception in further handlers for current request. 186 | /// 187 | /// 188 | /// The exception type 189 | /// 190 | /// 191 | /// The policy builder 192 | /// 193 | /// 194 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 195 | /// 196 | /// 197 | /// Policy builder 198 | /// 199 | public static IExceptionMapping DisableFurtherLog( 200 | this IExceptionMapping builder, int index = -1) 201 | where TException : Exception 202 | { 203 | builder.EnsureHandler(index); 204 | 205 | return builder; 206 | } 207 | 208 | // Set status code 209 | /// 210 | /// Configure response and pass control to next handler. It is recommended to finish chain using when response builder will be configured. 211 | /// 212 | /// 213 | /// The exception type 214 | /// 215 | /// 216 | /// The policy builder 217 | /// 218 | /// 219 | /// The begaviour in case response already started. 220 | /// 221 | /// 222 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 223 | /// 224 | /// 225 | /// The factory for response status code. By default 500 will be used, unless code was already set by another handler for current request. 226 | /// 227 | /// 228 | /// Response builder. 229 | /// 230 | public static IResponseHandlers Response( 231 | this IExceptionMapping builder, 232 | Func statusCodeFactory = null, 233 | ResponseAlreadyStartedBehaviour responseAlreadyStartedBehaviour = ResponseAlreadyStartedBehaviour.ReThrow, 234 | int index = -1) 235 | where TException : Exception 236 | { 237 | builder.Services.Configure>(responceOptions => 238 | { 239 | responceOptions.ResponseAlreadyStartedBehaviour = responseAlreadyStartedBehaviour; 240 | 241 | responceOptions.SetResponse.Add((context, exception) => 242 | { 243 | return RawResponseExceptionHandler.SetStatusCode(context, exception, statusCodeFactory); 244 | }); 245 | }); 246 | 247 | builder.EnsureHandler>(index); 248 | 249 | return builder as IResponseHandlers; 250 | } 251 | 252 | /// 253 | /// Modify response headers 254 | /// 255 | /// 256 | /// The exception type 257 | /// 258 | /// 259 | /// The policy builder 260 | /// 261 | /// 262 | /// Action for response headers modification 263 | /// 264 | /// 265 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 266 | /// 267 | /// 268 | /// Response builder 269 | /// 270 | public static IResponseHandlers Headers( 271 | this IResponseHandlers builder, Action settings, int index = -1) 272 | where TException : Exception 273 | { 274 | if (settings == null) 275 | { 276 | throw new ArgumentNullException(nameof(settings)); 277 | } 278 | 279 | builder.Services.Configure>(responceOptions => 280 | { 281 | responceOptions.SetResponse.Add((context, exception) => 282 | { 283 | settings.Invoke(context.Response.Headers, exception); 284 | return Task.CompletedTask; 285 | }); 286 | }); 287 | 288 | return builder; 289 | } 290 | 291 | /// 292 | /// Set response headers which revents response from being cached. 293 | /// 294 | /// 295 | /// The exception type 296 | /// 297 | /// 298 | /// The policy builder 299 | /// 300 | /// 301 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 302 | /// 303 | /// 304 | /// Policy builder 305 | /// 306 | public static IResponseHandlers ClearCacheHeaders( 307 | this IResponseHandlers builder, int index = -1) 308 | where TException : Exception 309 | { 310 | builder.Services.Configure>(responceOptions => 311 | { 312 | responceOptions.SetResponse.Add((context, exception) => 313 | { 314 | HttpResponse response = context.Response; 315 | 316 | response.Headers[HeaderNames.CacheControl] = "no-cache"; 317 | response.Headers[HeaderNames.Pragma] = "no-cache"; 318 | response.Headers[HeaderNames.Expires] = "-1"; 319 | response.Headers.Remove(HeaderNames.ETag); 320 | 321 | return Task.CompletedTask; 322 | }); 323 | }); 324 | 325 | return builder; 326 | } 327 | 328 | /// 329 | /// Set response body, close response builder and pass control to next handler. 330 | /// 331 | /// 332 | /// The exception type 333 | /// 334 | /// 335 | /// The policy builder 336 | /// 337 | /// 338 | /// Delegate to write to response stream. 339 | /// 340 | /// 341 | /// Handler index in the chain. Optional. By default handler added to the end of chain. 342 | /// 343 | /// 344 | /// Policy builder 345 | /// 346 | public static IResponseHandlers WithBody( 347 | this IResponseHandlers builder, Func settings, int index = -1) 348 | where TException : Exception 349 | { 350 | if (settings == null) 351 | { 352 | throw new ArgumentNullException(nameof(settings)); 353 | } 354 | 355 | builder.Services.Configure>(responceOptions => 356 | { 357 | responceOptions.SetResponse.Add((context, exception) => 358 | RawResponseExceptionHandler.SetBody(context, exception, settings)); 359 | }); 360 | 361 | return builder; 362 | } 363 | } 364 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Response/RawResponseExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Community.AspNetCore.ExceptionHandling.Handlers; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace Community.AspNetCore.ExceptionHandling.Response 12 | { 13 | public class RawResponseExceptionHandler : HandlerStrongType 14 | where TException : Exception 15 | { 16 | public const string StatusCodeSetKey = "5D1CFED34A39"; 17 | 18 | public const string BodySetKey = "6D1CFED34A39"; 19 | 20 | private readonly RawResponseHandlerOptions _options; 21 | 22 | private static readonly EventId ResponseHasStarted = new EventId(127, "ResponseAlreadyStarted"); 23 | 24 | public RawResponseExceptionHandler(IOptions> options, ILoggerFactory loggerFactory) 25 | : base(options.Value, loggerFactory) 26 | { 27 | _options = options.Value ?? throw new ArgumentNullException(nameof(options)); 28 | } 29 | 30 | protected override async Task HandleStrongType(HttpContext httpContext, TException exception) 31 | { 32 | if (httpContext.Response.HasStarted) 33 | { 34 | if (this._options.ResponseAlreadyStartedBehaviour == ResponseAlreadyStartedBehaviour.ReThrow) 35 | { 36 | this.Logger.LogError(ResponseHasStarted, 37 | "Unable to execute {handletType} handler, because respnse already started. Exception will be re-thrown.", 38 | this.GetType()); 39 | 40 | return HandlerResult.ReThrow; 41 | } 42 | else 43 | { 44 | return HandlerResult.NextHandler; 45 | } 46 | } 47 | 48 | await HandleResponseAsync(httpContext, exception); 49 | 50 | return HandlerResult.NextHandler; 51 | } 52 | 53 | protected virtual Task HandleResponseAsync(HttpContext httpContext, TException exception) 54 | { 55 | Task result = Task.CompletedTask; 56 | 57 | foreach (Func func in this._options.SetResponse) 58 | { 59 | result = result.ContinueWith(task => func(httpContext, exception)); 60 | } 61 | 62 | return result; 63 | } 64 | 65 | public static Task SetStatusCode(HttpContext context, TException exception, Func statusCodeFactory) 66 | { 67 | if(statusCodeFactory == null) 68 | { 69 | if (!context.Items.ContainsKey(StatusCodeSetKey)) 70 | { 71 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 72 | } 73 | } 74 | else 75 | { 76 | context.Response.StatusCode = statusCodeFactory.Invoke(exception); 77 | context.Items[StatusCodeSetKey] = true; 78 | } 79 | 80 | return Task.CompletedTask; 81 | } 82 | 83 | public static Task SetBody(HttpContext context, TException exception, Func settings) 84 | { 85 | if (!context.Items.ContainsKey(BodySetKey)) 86 | { 87 | context.Items[BodySetKey] = true; 88 | 89 | Stream stream = context.Response.Body; 90 | 91 | if (stream.CanSeek) 92 | { 93 | stream.Seek(0, SeekOrigin.Begin); 94 | stream.SetLength(0); 95 | } 96 | 97 | if (stream.CanWrite) 98 | { 99 | return settings(context.Request, stream, exception); 100 | } 101 | else 102 | { 103 | throw new InvalidOperationException("Unable to write to response stream"); 104 | } 105 | } 106 | 107 | return Task.CompletedTask; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Response/RawResponseHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Community.AspNetCore.ExceptionHandling.Handlers; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Community.AspNetCore.ExceptionHandling.Response 9 | { 10 | public class RawResponseHandlerOptions : HandlerWithLoggerOptions, 11 | IOptions> 12 | where TException : Exception 13 | { 14 | public List> SetResponse { get; set; } = new List>(); 15 | public RawResponseHandlerOptions Value => this; 16 | public ResponseAlreadyStartedBehaviour ResponseAlreadyStartedBehaviour { get; set; } = ResponseAlreadyStartedBehaviour.ReThrow; 17 | } 18 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/ResponseAlreadyStartedBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace Community.AspNetCore.ExceptionHandling 2 | { 3 | public enum ResponseAlreadyStartedBehaviour 4 | { 5 | /// 6 | /// Re Throw exception if response already strated 7 | /// 8 | ReThrow = 0, 9 | 10 | 11 | /// 12 | /// Skip current handler and go to next handler in the chain 13 | /// 14 | GoToNextHandler = 1 15 | } 16 | } -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Retry/RetryHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Community.AspNetCore.ExceptionHandling.Handlers; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace Community.AspNetCore.ExceptionHandling.Retry 8 | { 9 | class RetryHandler : IExceptionHandler 10 | where TException : Exception 11 | { 12 | private const string CurrentRetryGuardKey = "71DAAFEC7B56"; 13 | 14 | private readonly RetryHandlerOptions options; 15 | 16 | public RetryHandler(IOptions> options) 17 | { 18 | if (options == null) 19 | { 20 | throw new ArgumentNullException(nameof(options)); 21 | } 22 | 23 | this.options = options.Value; 24 | } 25 | 26 | public Task Handle(HttpContext httpContext, Exception exception) 27 | { 28 | if (httpContext.Response.HasStarted) 29 | { 30 | // Retry is not possible, so let's next handler decide 31 | return Task.FromResult(HandlerResult.NextHandler); 32 | } 33 | 34 | if (!httpContext.Items.ContainsKey(CurrentRetryGuardKey)) 35 | { 36 | httpContext.Items[CurrentRetryGuardKey] = 0; 37 | } 38 | 39 | if ((int)httpContext.Items[CurrentRetryGuardKey] < this.options.MaxRetryCount) 40 | { 41 | httpContext.Items[CurrentRetryGuardKey] = (int)httpContext.Items[CurrentRetryGuardKey] + 1; 42 | return Task.FromResult(HandlerResult.Retry); 43 | } 44 | else 45 | { 46 | // Retry is not possible, so let's next handler decide 47 | return Task.FromResult(HandlerResult.NextHandler); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Community.AspNetCore.ExceptionHandling/Retry/RetryHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace Community.AspNetCore.ExceptionHandling.Retry 5 | { 6 | public class RetryHandlerOptions : IOptions> 7 | where TException : Exception 8 | { 9 | public RetryHandlerOptions Value => this; 10 | 11 | /// 12 | /// Max retry count 13 | /// 14 | public int MaxRetryCount { get; set; } = 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Community.AspNetCore.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2015 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.AspNetCore.ExceptionHandling", "Community.AspNetCore.ExceptionHandling\Community.AspNetCore.ExceptionHandling.csproj", "{97ECCF71-494E-48FA-995A-AB1F13975A61}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.AspNetCore.ExceptionHandling.Integration", "Community.AspNetCore.ExceptionHandling.Integration\Community.AspNetCore.ExceptionHandling.Integration.csproj", "{393C6033-4255-43C3-896D-BFE30E264E4A}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C825A429-B51F-4E5D-BC78-5E0A390D0C38}" 11 | ProjectSection(SolutionItems) = preProject 12 | build.ps1 = build.ps1 13 | README.md = README.md 14 | Transitions.png = Transitions.png 15 | Transitions.vsdx = Transitions.vsdx 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.AspNetCore.ExceptionHandling.Mvc", "Community.AspNetCore.ExceptionHandling.Mvc\Community.AspNetCore.ExceptionHandling.Mvc.csproj", "{0D080E5A-9500-43AC-88CD-069947CFA5EF}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.AspNetCore.ExceptionHandling.NewtonsoftJson", "Community.AspNetCore.ExceptionHandling.NewtonsoftJson\Community.AspNetCore.ExceptionHandling.NewtonsoftJson.csproj", "{B3BAD0B5-15BC-46EE-A224-9DAF10376B81}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Community.AspNetCore.ExceptionHandling.Tests", "Community.AspNetCore.ExceptionHandling.Tests\Community.AspNetCore.ExceptionHandling.Tests.csproj", "{79A8C692-8B6B-4238-80C1-3F52AB6712C4}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {97ECCF71-494E-48FA-995A-AB1F13975A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {97ECCF71-494E-48FA-995A-AB1F13975A61}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {97ECCF71-494E-48FA-995A-AB1F13975A61}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {97ECCF71-494E-48FA-995A-AB1F13975A61}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {393C6033-4255-43C3-896D-BFE30E264E4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {393C6033-4255-43C3-896D-BFE30E264E4A}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {393C6033-4255-43C3-896D-BFE30E264E4A}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {393C6033-4255-43C3-896D-BFE30E264E4A}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {0D080E5A-9500-43AC-88CD-069947CFA5EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {0D080E5A-9500-43AC-88CD-069947CFA5EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {0D080E5A-9500-43AC-88CD-069947CFA5EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {0D080E5A-9500-43AC-88CD-069947CFA5EF}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {B3BAD0B5-15BC-46EE-A224-9DAF10376B81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {B3BAD0B5-15BC-46EE-A224-9DAF10376B81}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {B3BAD0B5-15BC-46EE-A224-9DAF10376B81}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {B3BAD0B5-15BC-46EE-A224-9DAF10376B81}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {79A8C692-8B6B-4238-80C1-3F52AB6712C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {79A8C692-8B6B-4238-80C1-3F52AB6712C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {79A8C692-8B6B-4238-80C1-3F52AB6712C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {79A8C692-8B6B-4238-80C1-3F52AB6712C4}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(ExtensibilityGlobals) = postSolution 55 | SolutionGuid = {18E2D6C5-7E06-4096-843F-534B6D926BE4} 56 | EndGlobalSection 57 | EndGlobal 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 IharYakimush 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core Exception Handling Middleware 2 | ASP.NET Core exception handling policies middleware. Allows to set a chain of exception handlers per exception type. OOTB handlers: log, retry, set responce headers and body 3 | 4 | ### Code Sample 5 | ```csharp 6 | public void ConfigureServices(IServiceCollection services) 7 | { 8 | services.AddMvc(); 9 | 10 | services.AddExceptionHandlingPolicies(options => 11 | { 12 | options.For().Rethrow(); 13 | 14 | options.For().Retry(ro => ro.MaxRetryCount = 2).NextPolicy(); 15 | 16 | options.For() 17 | .Response(e => 400) 18 | .Headers((h, e) => h["X-MyCustomHeader"] = e.Message) 19 | .WithBody((req,sw, exception) => 20 | { 21 | byte[] array = Encoding.UTF8.GetBytes(exception.ToString()); 22 | return sw.WriteAsync(array, 0, array.Length); 23 | }) 24 | .NextPolicy(); 25 | 26 | // Ensure that all exception types are handled by adding handler for generic exception at the end. 27 | options.For() 28 | .Log(lo => 29 | { 30 | lo.LogAction = (l, c, e) => l.LogError(e,"UnhandledException"); 31 | lo.Category = (context, exception) => "MyCategory"; 32 | }) 33 | .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler) 34 | .ClearCacheHeaders() 35 | .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path }) 36 | .Handled(); 37 | }); 38 | } 39 | 40 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 41 | { 42 | app.UseExceptionHandlingPolicies(); 43 | app.UseMvc(); 44 | } 45 | ``` 46 | 47 | ### Policy exception handler transitions 48 | When exception catched in middleware it try to apply handlers from first registered policy suitable for given exception. Policy contains a chain of handlers. Each handler perform some action and apply transition. To prevent re throw of exception handlers chain MUST ends with "Handled" transition. 49 | Following handlers currently supported: 50 | 51 | | Handler | Action | Transition | 52 | | ---------| ------------- | ------------- | 53 | | Rethrow | Apply ReThrow transition | ReThrow | 54 | | NextPolicy | Try to execute next policy suitable for given exception | NextPolicy | 55 | | Handled | Mark exception as handled to prevent it from bein re thrown | Handled | 56 | | Retry | Execute aspnetcore pipeline again if retry count not exceed limit | Retry (if retry limit not exceeded) or NextHandler | 57 | | Log | Log exception | NextHandler | 58 | | DisableFurtherLog | Prevent exception from being logged again in current middleware (for current request only) | NextHandler | 59 | | Response | Modify response (set status code, headers and body) depending on further response builder configuration | NextHandler | 60 | 61 | Sample of transitions: 62 | ![alt text](/Transitions.png) 63 | 64 | ### Nuget 65 | | Package | Comments | 66 | | ---------| ------------- | 67 | | Community.AspNetCore.ExceptionHandling | Main functionality | 68 | | Community.AspNetCore.ExceptionHandling.Mvc | Allow to use MVC IActionResult (including ObjectResult) in 'Response' handler | 69 | | Community.AspNetCore.ExceptionHandling.NewtonsoftJson | Allow to set Json serialized object as a response body in 'Response' handler. Use it only if 'Community.AspNetCore.ExceptionHandling.Mvc' usage not possible | 70 | -------------------------------------------------------------------------------- /Transitions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IharYakimush/asp-net-core-exception-handling/fa2126dab3e6a140d952211cbfb91aab0c7227eb/Transitions.png -------------------------------------------------------------------------------- /Transitions.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IharYakimush/asp-net-core-exception-handling/fa2126dab3e6a140d952211cbfb91aab0c7227eb/Transitions.vsdx -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | dotnet restore .\Community.AspNetCore.sln 2 | 3 | dotnet build .\Community.AspNetCore.sln -c Release -------------------------------------------------------------------------------- /sgn.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IharYakimush/asp-net-core-exception-handling/fa2126dab3e6a140d952211cbfb91aab0c7227eb/sgn.snk --------------------------------------------------------------------------------