├── .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 | 
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
--------------------------------------------------------------------------------