A global exception handling library for ASP.NET Core
61 |
GlobalExceptionHandler.NET allows you to configure application level exception handling as a convention within your ASP.NET Core application, opposed to explicitly handling exceptions within each controller action.
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/StatusCodeTests/StatusCodeAtRunTimeWithEnum.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using GlobalExceptionHandler.Tests.Exceptions;
5 | using GlobalExceptionHandler.Tests.Fixtures;
6 | using GlobalExceptionHandler.WebApi;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Newtonsoft.Json;
11 | using Shouldly;
12 | using Xunit;
13 |
14 | namespace GlobalExceptionHandler.Tests.WebApi.StatusCodeTests
15 | {
16 | public class StatusCodeAtRunTimeWithEnum : IClassFixture, IAsyncLifetime
17 | {
18 | private const string ApiProductNotFound = "/api/productnotfound";
19 | private readonly HttpClient _client;
20 | private HttpResponseMessage _response;
21 |
22 | public StatusCodeAtRunTimeWithEnum(WebApiServerFixture fixture)
23 | {
24 | // Arrange
25 | var webHost = fixture.CreateWebHostWithMvc();
26 | webHost.Configure(app =>
27 | {
28 | app.UseGlobalExceptionHandler(x =>
29 | {
30 | x.ContentType = "application/json";
31 | x.Map().ToStatusCode(ex => ex.StatusCodeEnum);
32 | x.ResponseBody(c => JsonConvert.SerializeObject(new TestResponse
33 | {
34 | Message = c.Message
35 | }));
36 | });
37 |
38 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new HttpNotFoundException("Record not found")); });
39 | });
40 |
41 | _client = new TestServer(webHost).CreateClient();
42 | }
43 |
44 | public async Task InitializeAsync()
45 | {
46 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
47 | }
48 |
49 | [Fact]
50 | public void Returns_correct_response_type()
51 | {
52 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
53 | }
54 |
55 | [Fact]
56 | public void Returns_correct_status_code()
57 | {
58 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
59 | }
60 |
61 | public Task DisposeAsync()
62 | => Task.CompletedTask;
63 | }
64 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/StatusCodeTests/StatusCodeWithEnum.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.Tests.Fixtures;
6 | using GlobalExceptionHandler.WebApi;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Newtonsoft.Json;
11 | using Shouldly;
12 | using Xunit;
13 |
14 | namespace GlobalExceptionHandler.Tests.WebApi.StatusCodeTests
15 | {
16 | public class BasicTestsEnum : IClassFixture, IAsyncLifetime
17 | {
18 | private const string ApiProductNotFound = "/api/productnotfound";
19 | private readonly HttpClient _client;
20 | private HttpResponseMessage _response;
21 |
22 | public BasicTestsEnum(WebApiServerFixture fixture)
23 | {
24 | // Arrange
25 | var webHost = fixture.CreateWebHostWithMvc();
26 | webHost.Configure(app =>
27 | {
28 | app.UseGlobalExceptionHandler(x =>
29 | {
30 | x.ContentType = "application/json";
31 | x.Map().ToStatusCode(HttpStatusCode.BadRequest);
32 | x.ResponseBody(c => JsonConvert.SerializeObject(new TestResponse
33 | {
34 | Message = c.Message
35 | }));
36 | });
37 |
38 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new ArgumentException("Invalid request")); });
39 | });
40 |
41 | _client = new TestServer(webHost).CreateClient();
42 | }
43 |
44 | public async Task InitializeAsync()
45 | {
46 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
47 | }
48 |
49 | [Fact]
50 | public async Task Returns_correct_response_type()
51 | {
52 | var response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
53 | response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
54 | }
55 |
56 | [Fact]
57 | public void Returns_correct_status_code()
58 | {
59 | _response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
60 | }
61 |
62 | public Task DisposeAsync()
63 | => Task.CompletedTask;
64 | }
65 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/GlobalFormatterTests/BasicTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.Tests.Fixtures;
6 | using GlobalExceptionHandler.WebApi;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Newtonsoft.Json;
11 | using Shouldly;
12 | using Xunit;
13 |
14 | namespace GlobalExceptionHandler.Tests.WebApi.GlobalFormatterTests
15 | {
16 | public class BasicTests : IClassFixture, IAsyncLifetime
17 | {
18 | private const string ApiProductNotFound = "/api/productnotfound";
19 | private readonly HttpClient _client;
20 | private HttpResponseMessage _response;
21 |
22 | public BasicTests(WebApiServerFixture fixture)
23 | {
24 | // Arrange
25 | var webHost = fixture.CreateWebHostWithMvc();
26 | webHost.Configure(app =>
27 | {
28 | app.UseGlobalExceptionHandler(x =>
29 | {
30 | x.ContentType = "application/json";
31 | x.ResponseBody(c => JsonConvert.SerializeObject(new TestResponse
32 | {
33 | Message = c.Message
34 | }));
35 | });
36 |
37 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new ArgumentException("Invalid request")); });
38 | });
39 |
40 | _client = new TestServer(webHost).CreateClient();
41 | }
42 |
43 | [Fact]
44 | public void Returns_correct_response_type()
45 | {
46 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
47 | }
48 |
49 | [Fact]
50 | public void Returns_correct_status_code()
51 | {
52 | _response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError);
53 | }
54 |
55 | [Fact]
56 | public async Task Returns_empty_body()
57 | {
58 | var content = await _response.Content.ReadAsStringAsync();
59 | content.ShouldBe("{\"Message\":\"Invalid request\"}");
60 | }
61 |
62 | public async Task InitializeAsync()
63 | {
64 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
65 | }
66 |
67 | public Task DisposeAsync()
68 | => Task.CompletedTask;
69 | }
70 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/LoggerTests/HandledExceptionLoggerTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using GlobalExceptionHandler.Tests.Fixtures;
5 | using GlobalExceptionHandler.WebApi;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Shouldly;
11 | using Xunit;
12 |
13 | namespace GlobalExceptionHandler.Tests.WebApi.LoggerTests
14 | {
15 | public class HandledExceptionLoggerTests : IClassFixture, IAsyncLifetime
16 | {
17 | private readonly TestServer _server;
18 | private Type _matchedException;
19 | private Exception _exception;
20 | private const string RequestUri = "/api/productnotfound";
21 |
22 | public HandledExceptionLoggerTests(WebApiServerFixture fixture)
23 | {
24 | // Arrange
25 | var webHost = fixture.CreateWebHostWithMvc();
26 | webHost.Configure(app =>
27 | {
28 | app.UseGlobalExceptionHandler(x =>
29 | {
30 | x.OnException((context, _) =>
31 | {
32 | _matchedException = context.ExceptionMatched;
33 | _exception = context.Exception;
34 |
35 | return Task.CompletedTask;
36 | });
37 | x.Map().ToStatusCode(StatusCodes.Status404NotFound).WithBody((e, c, h) => Task.CompletedTask);
38 | });
39 |
40 | app.Map(RequestUri, config =>
41 | {
42 | config.Run(context => throw new ArgumentException("Invalid request"));
43 | });
44 | });
45 |
46 | _server = new TestServer(webHost);
47 | }
48 |
49 | public async Task InitializeAsync()
50 | {
51 | using var client = _server.CreateClient();
52 | var requestMessage = new HttpRequestMessage(new HttpMethod("GET"), RequestUri);
53 | await client.SendAsync(requestMessage);
54 | }
55 |
56 | [Fact]
57 | public void ExceptionTypeMatches()
58 | => _matchedException.FullName.ShouldBe("System.ArgumentException");
59 |
60 | [Fact]
61 | public void ExceptionIsCorrect()
62 | => _exception.ShouldBeOfType();
63 |
64 | public Task DisposeAsync()
65 | => Task.CompletedTask;
66 | }
67 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/LoggerTests/UnhandledExceptionLoggerTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using GlobalExceptionHandler.Tests.Fixtures;
5 | using GlobalExceptionHandler.WebApi;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Shouldly;
11 | using Xunit;
12 |
13 | namespace GlobalExceptionHandler.Tests.WebApi.LoggerTests
14 | {
15 | public class UnhandledExceptionLoggerTests : IClassFixture, IAsyncLifetime
16 | {
17 | private readonly TestServer _server;
18 | private Type _matchedException;
19 | private Exception _exception;
20 | private const string RequestUri = "/api/productnotfound";
21 |
22 | public UnhandledExceptionLoggerTests(WebApiServerFixture fixture)
23 | {
24 | // Arrange
25 | var webHost = fixture.CreateWebHostWithMvc();
26 | webHost.Configure(app =>
27 | {
28 | app.UseGlobalExceptionHandler(x =>
29 | {
30 | x.OnException((context, _) =>
31 | {
32 | _matchedException = context.ExceptionMatched;
33 | _exception = context.Exception;
34 |
35 | return Task.CompletedTask;
36 | });
37 | x.Map().ToStatusCode(StatusCodes.Status404NotFound).WithBody((e, c, h) => Task.CompletedTask);
38 | });
39 |
40 | app.Map(RequestUri, config =>
41 | {
42 | config.Run(context => throw new NotImplementedException("Method not implemented"));
43 | });
44 | });
45 |
46 | _server = new TestServer(webHost);
47 | }
48 |
49 | public async Task InitializeAsync()
50 | {
51 | using var client = _server.CreateClient();
52 | var requestMessage = new HttpRequestMessage(new HttpMethod("GET"), RequestUri);
53 | await client.SendAsync(requestMessage);
54 | }
55 |
56 | [Fact]
57 | public void ExceptionMatchIsNotSet()
58 | => _matchedException.ShouldBeNull();
59 |
60 | [Fact]
61 | public void ExceptionTypeIsCorrect()
62 | => _exception.ShouldBeOfType();
63 |
64 | public Task DisposeAsync()
65 | => Task.CompletedTask;
66 | }
67 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/StatusCodeTests/StatusCodeAtRunTimeWithInt.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using GlobalExceptionHandler.Tests.Exceptions;
5 | using GlobalExceptionHandler.Tests.Fixtures;
6 | using GlobalExceptionHandler.WebApi;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Newtonsoft.Json;
11 | using Shouldly;
12 | using Xunit;
13 |
14 | namespace GlobalExceptionHandler.Tests.WebApi.StatusCodeTests
15 | {
16 | public class StatusCodeAtRunTimeWithInt : IClassFixture, IAsyncLifetime
17 | {
18 | private const string ApiProductNotFound = "/api/productnotfound";
19 | private readonly HttpClient _client;
20 | private HttpResponseMessage _response;
21 |
22 | public StatusCodeAtRunTimeWithInt(WebApiServerFixture fixture)
23 | {
24 | // Arrange
25 | var webHost = fixture.CreateWebHostWithMvc();
26 | webHost.Configure(app =>
27 | {
28 | app.UseGlobalExceptionHandler(x =>
29 | {
30 | x.ContentType = "application/json";
31 | x.Map().ToStatusCode(ex => ex.StatusCodeInt);
32 | x.ResponseBody(c => JsonConvert.SerializeObject(new TestResponse
33 | {
34 | Message = c.Message
35 | }));
36 | });
37 |
38 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new HttpNotFoundException("Record not found")); });
39 | });
40 |
41 | _client = new TestServer(webHost).CreateClient();
42 | }
43 |
44 | public async Task InitializeAsync()
45 | {
46 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
47 | }
48 |
49 | [Fact]
50 | public void Returns_correct_response_type()
51 | {
52 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
53 | }
54 |
55 | [Fact]
56 | public async Task Returns_correct_status_code()
57 | {
58 | var response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
59 | response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
60 | }
61 |
62 | public Task DisposeAsync()
63 | => Task.CompletedTask;
64 | }
65 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/GlobalFormatterTests/FallBackResponseTest.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using GlobalExceptionHandler.Tests.Exceptions;
5 | using GlobalExceptionHandler.Tests.Fixtures;
6 | using GlobalExceptionHandler.WebApi;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.Http;
10 | using Microsoft.AspNetCore.TestHost;
11 | using Newtonsoft.Json;
12 | using Shouldly;
13 | using Xunit;
14 |
15 | namespace GlobalExceptionHandler.Tests.WebApi.GlobalFormatterTests
16 | {
17 | public class FallBackResponseTest : IClassFixture, IAsyncLifetime
18 | {
19 | private const string ApiProductNotFound = "/api/productnotfound";
20 | private readonly HttpClient _client;
21 | private HttpResponseMessage _response;
22 |
23 | public FallBackResponseTest(WebApiServerFixture fixture)
24 | {
25 | // Arrange
26 | var webHost = fixture.CreateWebHostWithMvc();
27 | webHost.Configure(app =>
28 | {
29 | app.UseGlobalExceptionHandler(x => {
30 | x.ContentType = "application/json";
31 | x.ResponseBody(s => JsonConvert.SerializeObject(new
32 | {
33 | Message = "An error occured whilst processing your request"
34 | }));
35 | x.Map().ToStatusCode(StatusCodes.Status404NotFound);
36 | });
37 |
38 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new RecordNotFoundException()); });
39 | });
40 |
41 | _client = new TestServer(webHost).CreateClient();
42 | }
43 |
44 | [Fact]
45 | public void Returns_correct_response_type()
46 | {
47 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
48 | }
49 |
50 | [Fact]
51 | public void Returns_correct_status_code()
52 | {
53 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
54 | }
55 |
56 | [Fact]
57 | public async Task Returns_global_exception_message()
58 | {
59 | var content = await _response.Content.ReadAsStringAsync();
60 | content.ShouldBe("{\"Message\":\"An error occured whilst processing your request\"}");
61 | }
62 |
63 | public async Task InitializeAsync()
64 | {
65 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
66 | }
67 |
68 | public Task DisposeAsync()
69 | => Task.CompletedTask;
70 | }
71 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/LoggerTests/UnhandledExceptionTests.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http;
2 | using System.Threading.Tasks;
3 | using Divergic.Logging.Xunit;
4 | using GlobalExceptionHandler.Tests.Fixtures;
5 | using GlobalExceptionHandler.WebApi;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Microsoft.Extensions.Logging;
11 | using Newtonsoft.Json;
12 | using Shouldly;
13 | using Xunit;
14 | using Xunit.Abstractions;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.LoggerTests
17 | {
18 | public class UnhandledExceptionTests : IClassFixture, IAsyncLifetime
19 | {
20 | private readonly ITestOutputHelper _output;
21 | private readonly TestServer _server;
22 | private const string RequestUri = "/api/productnotfound";
23 |
24 | public UnhandledExceptionTests(WebApiServerFixture fixture, ITestOutputHelper output)
25 | {
26 | _output = output;
27 | // Arrange
28 | var webHost = fixture.CreateWebHostWithMvc();
29 | webHost.Configure(app =>
30 | {
31 | app.UseGlobalExceptionHandler(x =>
32 | {
33 | x.OnException((ExceptionContext context, ILogger logger) =>
34 | {
35 |
36 | return Task.CompletedTask;
37 | });
38 | x.ResponseBody(c => JsonConvert.SerializeObject(new TestResponse
39 | {
40 | Message = c.Message
41 | }));
42 | x.Map().ToStatusCode(StatusCodes.Status404NotFound).WithBody((e, c, h) => Task.CompletedTask);
43 | }, LogFactory.Create(output));
44 |
45 | app.Map(RequestUri, c => c.Run(context => throw new HttpRequestException("Something went wrong")));
46 | });
47 |
48 | _server = new TestServer(webHost);
49 | }
50 |
51 | public async Task InitializeAsync()
52 | {
53 | using (var client = _server.CreateClient())
54 | {
55 | var requestMessage = new HttpRequestMessage(new HttpMethod("GET"), RequestUri);
56 | await client.SendAsync(requestMessage);
57 | }
58 | }
59 |
60 | [Fact]
61 | public void Unhandled_exception_is_thrown()
62 | {
63 | // The ExceptionHandling middleware returns an unhandled exception
64 | // See Microsoft.AspNetCore.Diagnostics.ExceptionHandlingMiddleware
65 | true.ShouldBe(true);
66 | }
67 |
68 | public Task DisposeAsync()
69 | => Task.CompletedTask;
70 | }
71 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/GlobalFormatterTests/OverrideFallbackResponseTest.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using GlobalExceptionHandler.Tests.Exceptions;
5 | using GlobalExceptionHandler.Tests.Fixtures;
6 | using GlobalExceptionHandler.WebApi;
7 | using Microsoft.AspNetCore.Builder;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.Http;
10 | using Microsoft.AspNetCore.TestHost;
11 | using Newtonsoft.Json;
12 | using Shouldly;
13 | using Xunit;
14 |
15 | namespace GlobalExceptionHandler.Tests.WebApi.GlobalFormatterTests
16 | {
17 | public class OverrideFallbackResponseTest : IClassFixture, IAsyncLifetime
18 | {
19 | private const string ApiProductNotFound = "/api/productnotfound";
20 | private readonly HttpClient _client;
21 | private HttpResponseMessage _response;
22 |
23 | public OverrideFallbackResponseTest(WebApiServerFixture fixture)
24 | {
25 | // Arrange
26 | var webHost = fixture.CreateWebHostWithMvc();
27 | webHost.Configure(app =>
28 | {
29 | app.UseGlobalExceptionHandler(x => {
30 | x.ContentType = "application/json";
31 | x.ResponseBody(s => JsonConvert.SerializeObject(new
32 | {
33 | Message = "An error occured whilst processing your request"
34 | }));
35 |
36 | x.Map().ToStatusCode(StatusCodes.Status404NotFound).WithBody((e, c) => JsonConvert.SerializeObject(new {e.Message}));
37 | });
38 |
39 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new RecordNotFoundException("Record could not be found")); });
40 | });
41 |
42 | _client = new TestServer(webHost).CreateClient();
43 | }
44 |
45 | [Fact]
46 | public void Returns_correct_response_type()
47 | {
48 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
49 | }
50 |
51 | [Fact]
52 | public void Returns_correct_status_code()
53 | {
54 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
55 | }
56 |
57 | [Fact]
58 | public async Task Returns_global_exception_message()
59 | {
60 | var content = await _response.Content.ReadAsStringAsync();
61 | content.ShouldBe("{\"Message\":\"Record could not be found\"}");
62 | }
63 |
64 | public async Task InitializeAsync()
65 | {
66 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
67 | }
68 |
69 | public Task DisposeAsync()
70 | => Task.CompletedTask;
71 | }
72 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/GlobalFormatterTests/BasicWithOverrideTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.Tests.Exceptions;
6 | using GlobalExceptionHandler.Tests.Fixtures;
7 | using GlobalExceptionHandler.WebApi;
8 | using Microsoft.AspNetCore.Builder;
9 | using Microsoft.AspNetCore.Hosting;
10 | using Microsoft.AspNetCore.Http;
11 | using Microsoft.AspNetCore.TestHost;
12 | using Newtonsoft.Json;
13 | using Shouldly;
14 | using Xunit;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.GlobalFormatterTests
17 | {
18 | public class BasicWithOverrideTests : IClassFixture, IAsyncLifetime
19 | {
20 | private const string ApiProductNotFound = "/api/productnotfound";
21 | private readonly HttpClient _client;
22 | private HttpResponseMessage _response;
23 |
24 | public BasicWithOverrideTests(WebApiServerFixture fixture)
25 | {
26 | // Arrange
27 | var webHost = fixture.CreateWebHostWithMvc();
28 | webHost.Configure(app =>
29 | {
30 | app.UseGlobalExceptionHandler(x =>
31 | {
32 | x.ContentType = "application/json";
33 | x.Map().ToStatusCode(StatusCodes.Status400BadRequest);
34 | x.ResponseBody(exception => JsonConvert.SerializeObject(new
35 | {
36 | error = new
37 | {
38 | message = "Something went wrong"
39 | }
40 | }));
41 | });
42 |
43 | app.Map(ApiProductNotFound, config =>
44 | {
45 | config.Run(context => throw new ArgumentException("Invalid request"));
46 | });
47 | });
48 |
49 | _client = new TestServer(webHost).CreateClient();
50 | }
51 |
52 | [Fact]
53 | public void Returns_correct_response_type()
54 | {
55 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
56 | }
57 |
58 | [Fact]
59 | public void Returns_correct_status_code()
60 | {
61 | _response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError);
62 | }
63 |
64 | [Fact]
65 | public async Task Returns_correct_body()
66 | {
67 | var content = await _response.Content.ReadAsStringAsync();
68 | content.ShouldBe(@"{""error"":{""message"":""Something went wrong""}}");
69 | }
70 |
71 | public async Task InitializeAsync()
72 | {
73 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound));
74 | }
75 |
76 | public Task DisposeAsync()
77 | => Task.CompletedTask;
78 | }
79 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/ContentNegotiationTests/CustomFormatter/PlainTextResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.ContentNegotiation;
6 | using GlobalExceptionHandler.Tests.Exceptions;
7 | using GlobalExceptionHandler.Tests.Fixtures;
8 | using GlobalExceptionHandler.WebApi;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.AspNetCore.Hosting;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.AspNetCore.TestHost;
13 | using Shouldly;
14 | using Xunit;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.ContentNegotiationTests.CustomFormatter
17 | {
18 | public class PlainTextResponse : IClassFixture, IAsyncLifetime
19 | {
20 | private const string ApiProductNotFound = "/api/productnotfound";
21 | private readonly HttpRequestMessage _requestMessage;
22 | private readonly HttpClient _client;
23 | private HttpResponseMessage _response;
24 |
25 | public PlainTextResponse(WebApiServerFixture fixture)
26 | {
27 | // Arrange
28 |
29 | var webHost = fixture.CreateWebHostWithMvc();
30 | webHost.Configure(app =>
31 | {
32 | app.UseGlobalExceptionHandler(x =>
33 | {
34 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
35 | .WithBody((e, c, h) => c.WriteAsyncObject(e.Message));
36 | });
37 |
38 | app.Map(ApiProductNotFound, config =>
39 | {
40 | config.Run(context => throw new RecordNotFoundException("Record could not be found"));
41 | });
42 | });
43 |
44 | _requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
45 | _requestMessage.Headers.Accept.Clear();
46 | _requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain"));
47 |
48 | _client = new TestServer(webHost).CreateClient();
49 | }
50 |
51 | [Fact]
52 | public void Returns_correct_response_type()
53 | {
54 | _response.Content.Headers.ContentType.MediaType.ShouldBe("text/plain");
55 | }
56 |
57 | [Fact]
58 | public void Returns_correct_status_code()
59 | {
60 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
61 | }
62 |
63 | [Fact]
64 | public async Task Returns_correct_body()
65 | {
66 | var content = await _response.Content.ReadAsStringAsync();
67 | content.ShouldContain("Record could not be found");
68 | }
69 |
70 | public async Task InitializeAsync()
71 | {
72 | _response = await _client.SendAsync(_requestMessage);
73 | }
74 |
75 | public Task DisposeAsync()
76 | => Task.CompletedTask;
77 | }
78 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/ContentNegotiationTests/CustomFormatter/XmlResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.ContentNegotiation;
6 | using GlobalExceptionHandler.Tests.Exceptions;
7 | using GlobalExceptionHandler.Tests.Fixtures;
8 | using GlobalExceptionHandler.WebApi;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.AspNetCore.Hosting;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.AspNetCore.TestHost;
13 | using Shouldly;
14 | using Xunit;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.ContentNegotiationTests.CustomFormatter
17 | {
18 | public class XmlResponse : IClassFixture, IAsyncLifetime
19 | {
20 | private const string ApiProductNotFound = "/api/productnotfound";
21 | private readonly HttpClient _client;
22 | private HttpResponseMessage _response;
23 |
24 | public XmlResponse(WebApiServerFixture fixture)
25 | {
26 | // Arrange
27 | var webHost = fixture.CreateWebHostWithXmlFormatters();
28 | webHost.Configure(app =>
29 | {
30 | app.UseGlobalExceptionHandler(x =>
31 | {
32 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
33 | .WithBody(new TestResponse
34 | {
35 | Message = "An exception occured"
36 | });
37 | });
38 |
39 | app.Map(ApiProductNotFound, config =>
40 | {
41 | config.Run(context => throw new RecordNotFoundException("Record could not be found"));
42 | });
43 | });
44 |
45 | _client = new TestServer(webHost).CreateClient();
46 | }
47 |
48 | [Fact]
49 | public void Returns_correct_response_type()
50 | {
51 | _response.Content.Headers.ContentType.MediaType.ShouldBe("text/xml");
52 | }
53 |
54 | [Fact]
55 | public void Returns_correct_status_code()
56 | {
57 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
58 | }
59 |
60 | [Fact]
61 | public async Task Returns_correct_body()
62 | {
63 | var content = await _response.Content.ReadAsStringAsync();
64 | content.ShouldContain("An exception occured");
65 | }
66 |
67 | public async Task InitializeAsync()
68 | {
69 | var requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
70 | requestMessage.Headers.Accept.Clear();
71 | requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml"));
72 |
73 | _response = await _client.SendAsync(requestMessage);
74 | }
75 |
76 | public Task DisposeAsync()
77 | => Task.CompletedTask;
78 | }
79 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/ContentNegotiationTests/GlobalFormatter/XmlResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.Tests.Exceptions;
6 | using GlobalExceptionHandler.Tests.Fixtures;
7 | using GlobalExceptionHandler.WebApi;
8 | using Microsoft.AspNetCore.Builder;
9 | using Microsoft.AspNetCore.Hosting;
10 | using Microsoft.AspNetCore.Http;
11 | using Microsoft.AspNetCore.TestHost;
12 | using Shouldly;
13 | using Xunit;
14 |
15 | namespace GlobalExceptionHandler.Tests.WebApi.ContentNegotiationTests.GlobalFormatter
16 | {
17 | public class XmlResponse : IClassFixture, IAsyncLifetime
18 | {
19 | private readonly HttpClient _client;
20 | private HttpResponseMessage _response;
21 | private const string ContentType = "application/xml";
22 | private const string ApiProductNotFound = "/api/productnotfound";
23 | private const string ErrorMessage = "Record could not be found";
24 |
25 | public XmlResponse(WebApiServerFixture fixture)
26 | {
27 | // Arrange
28 | var webHost = fixture.CreateWebHostWithXmlFormatters();
29 | webHost.Configure(app =>
30 | {
31 | app.UseGlobalExceptionHandler(x =>
32 | {
33 | x.DefaultStatusCode = StatusCodes.Status404NotFound;
34 | x.ResponseBody(ex => new TestResponse
35 | {
36 | Message = ex.Message
37 | });
38 | });
39 |
40 | app.Map(ApiProductNotFound, config =>
41 | {
42 | config.Run(context => throw new RecordNotFoundException(ErrorMessage));
43 | });
44 | });
45 |
46 |
47 | _client = new TestServer(webHost).CreateClient();
48 | }
49 |
50 | [Fact]
51 | public void Returns_correct_response_type()
52 | {
53 | _response.Content.Headers.ContentType.MediaType.ShouldBe(ContentType);
54 | }
55 |
56 | [Fact]
57 | public void Returns_correct_status_code()
58 | {
59 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
60 | }
61 |
62 | [Fact]
63 | public async Task Returns_correct_body()
64 | {
65 | var content = await _response.Content.ReadAsStringAsync();
66 | content.ShouldContain($"{ErrorMessage}");
67 | }
68 |
69 | public async Task InitializeAsync()
70 | {
71 | var requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
72 | requestMessage.Headers.Accept.Clear();
73 | requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
74 |
75 | _response = await _client.SendAsync(requestMessage);
76 | }
77 |
78 | public Task DisposeAsync()
79 | => Task.CompletedTask;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/ContentNegotiationTests/GlobalFormatter/JsonResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.Tests.Exceptions;
6 | using GlobalExceptionHandler.Tests.Fixtures;
7 | using GlobalExceptionHandler.WebApi;
8 | using Microsoft.AspNetCore.Builder;
9 | using Microsoft.AspNetCore.Hosting;
10 | using Microsoft.AspNetCore.Http;
11 | using Microsoft.AspNetCore.TestHost;
12 | using Shouldly;
13 | using Xunit;
14 |
15 | namespace GlobalExceptionHandler.Tests.WebApi.ContentNegotiationTests.GlobalFormatter
16 | {
17 | public class JsonResponse : IClassFixture, IAsyncLifetime
18 | {
19 | private readonly HttpRequestMessage _requestMessage;
20 | private readonly HttpClient _client;
21 | private HttpResponseMessage _response;
22 | private const string ApiProductNotFound = "/api/productnotfound";
23 | private const string ErrorMessage = "Record could not be found";
24 |
25 | public JsonResponse(WebApiServerFixture fixture)
26 | {
27 | // Arrange
28 | var webHost = fixture.CreateWebHostWithMvc();
29 | webHost.Configure(app =>
30 | {
31 | app.UseGlobalExceptionHandler(x =>
32 | {
33 | x.ContentType = "application/json";
34 | x.DefaultStatusCode = StatusCodes.Status404NotFound;
35 | x.ResponseBody(ex => new TestResponse
36 | {
37 | Message = ex.Message
38 | });
39 | });
40 |
41 | app.Map(ApiProductNotFound, config =>
42 | {
43 | config.Run(context => throw new RecordNotFoundException(ErrorMessage));
44 | });
45 | });
46 |
47 | _requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
48 | _requestMessage.Headers.Accept.Clear();
49 | _requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
50 |
51 | _client = new TestServer(webHost).CreateClient();
52 | }
53 |
54 | [Fact]
55 | public void Returns_correct_response_type()
56 | {
57 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
58 | }
59 |
60 | [Fact]
61 | public void Returns_correct_status_code()
62 | {
63 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
64 | }
65 |
66 | [Fact]
67 | public async Task Returns_correct_body()
68 | {
69 | var content = await _response.Content.ReadAsStringAsync();
70 | content.ShouldContain("{\"message\":\"" + ErrorMessage + "\"}");
71 | }
72 |
73 | public async Task InitializeAsync()
74 | => _response = await _client.SendAsync(_requestMessage);
75 |
76 | public Task DisposeAsync()
77 | => Task.CompletedTask;
78 | }
79 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/ContentNegotiationTests/CustomFormatter/XmlResponseWithException.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.ContentNegotiation;
6 | using GlobalExceptionHandler.Tests.Exceptions;
7 | using GlobalExceptionHandler.Tests.Fixtures;
8 | using GlobalExceptionHandler.WebApi;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.AspNetCore.Hosting;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.AspNetCore.TestHost;
13 | using Shouldly;
14 | using Xunit;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.ContentNegotiationTests.CustomFormatter
17 | {
18 | public class XmlResponseWithException : IClassFixture, IAsyncLifetime
19 | {
20 | private const string ApiProductNotFound = "/api/productnotfound";
21 | private readonly HttpRequestMessage _requestMessage;
22 | private readonly HttpClient _client;
23 | private HttpResponseMessage _response;
24 |
25 | public XmlResponseWithException(WebApiServerFixture fixture)
26 | {
27 | // Arrange
28 | var webHost = fixture.CreateWebHostWithXmlFormatters();
29 | webHost.Configure(app =>
30 | {
31 | app.UseGlobalExceptionHandler(x =>
32 | {
33 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
34 | .WithBody(e => new TestResponse
35 | {
36 | Message = "An exception occured"
37 | });
38 | });
39 |
40 | app.Map(ApiProductNotFound, config =>
41 | {
42 | config.Run(context => throw new RecordNotFoundException("Record could not be found"));
43 | });
44 | });
45 |
46 | _requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
47 | _requestMessage.Headers.Accept.Clear();
48 | _requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml"));
49 |
50 | _client = new TestServer(webHost).CreateClient();
51 | }
52 |
53 | [Fact]
54 | public void Returns_correct_response_type()
55 | {
56 | _response.Content.Headers.ContentType.MediaType.ShouldBe("text/xml");
57 | }
58 |
59 | [Fact]
60 | public void Returns_correct_status_code()
61 | {
62 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
63 | }
64 |
65 | [Fact]
66 | public async Task Returns_correct_body()
67 | {
68 | var content = await _response.Content.ReadAsStringAsync();
69 | content.ShouldContain("An exception occured");
70 | }
71 |
72 | public async Task InitializeAsync()
73 | {
74 | _response = await _client.SendAsync(_requestMessage);
75 | }
76 |
77 | public Task DisposeAsync()
78 | => Task.CompletedTask;
79 | }
80 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/ContentNegotiationTests/CustomFormatter/JsonResponse.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.ContentNegotiation;
6 | using GlobalExceptionHandler.Tests.Exceptions;
7 | using GlobalExceptionHandler.Tests.Fixtures;
8 | using GlobalExceptionHandler.WebApi;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.AspNetCore.Hosting;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.AspNetCore.TestHost;
13 | using Shouldly;
14 | using Xunit;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.ContentNegotiationTests.CustomFormatter
17 | {
18 | public class JsonResponse : IClassFixture, IAsyncLifetime
19 | {
20 | private readonly HttpRequestMessage _requestMessage;
21 | private readonly HttpClient _client;
22 | private HttpResponseMessage _response;
23 | private const string ContentType = "application/json";
24 | private const string ApiProductNotFound = "/api/productnotfound";
25 |
26 | public JsonResponse(WebApiServerFixture fixture)
27 | {
28 | // ArranLge
29 | var webHost = fixture.CreateWebHostWithMvc();
30 | webHost.Configure(app =>
31 | {
32 | app.UseGlobalExceptionHandler(x =>
33 | {
34 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
35 | .WithBody(y => new TestResponse
36 | {
37 | Message = "An exception occured"
38 | });
39 | });
40 |
41 | app.Map(ApiProductNotFound, config =>
42 | {
43 | config.Run(context => throw new RecordNotFoundException("Record could not be found"));
44 | });
45 | });
46 |
47 | _requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
48 | _requestMessage.Headers.Accept.Clear();
49 | _requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
50 |
51 | _client = new TestServer(webHost).CreateClient();
52 | }
53 |
54 | [Fact]
55 | public void Returns_correct_response_type()
56 | {
57 | _response.Content.Headers.ContentType.MediaType.ShouldBe(ContentType);
58 | }
59 |
60 | [Fact]
61 | public void Returns_correct_status_code()
62 | {
63 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
64 | }
65 |
66 | [Fact]
67 | public async Task Returns_correct_body()
68 | {
69 | var content = await _response.Content.ReadAsStringAsync();
70 | content.ShouldContain("{\"message\":\"An exception occured\"}");
71 | }
72 |
73 | public async Task InitializeAsync()
74 | {
75 | _response = await _client.SendAsync(_requestMessage);
76 | }
77 |
78 | public Task DisposeAsync()
79 | => Task.CompletedTask;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/ContentNegotiationTests/CustomFormatter/JsonResponseWithException.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http;
3 | using System.Net.Http.Headers;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.ContentNegotiation;
6 | using GlobalExceptionHandler.Tests.Exceptions;
7 | using GlobalExceptionHandler.Tests.Fixtures;
8 | using GlobalExceptionHandler.WebApi;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.AspNetCore.Hosting;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.AspNetCore.TestHost;
13 | using Shouldly;
14 | using Xunit;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.ContentNegotiationTests.CustomFormatter
17 | {
18 | public class JsonResponseWithException : IClassFixture, IAsyncLifetime
19 | {
20 | private const string ApiProductNotFound = "/api/productnotfound";
21 | private readonly HttpRequestMessage _requestMessage;
22 | private readonly HttpClient _client;
23 | private HttpResponseMessage _response;
24 |
25 | public JsonResponseWithException(WebApiServerFixture fixture)
26 | {
27 | // Arrange
28 | var webHost = fixture.CreateWebHostWithMvc();
29 | webHost.Configure(app =>
30 | {
31 | app.UseGlobalExceptionHandler(x =>
32 | {
33 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
34 | .WithBody(e => new TestResponse
35 | {
36 | Message = "An exception occured"
37 | });
38 | });
39 |
40 | app.Map(ApiProductNotFound, config =>
41 | {
42 | config.Run(context => throw new RecordNotFoundException("Record could not be found"));
43 | });
44 | });
45 |
46 | // Act
47 | _requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
48 | _requestMessage.Headers.Accept.Clear();
49 | _requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
50 |
51 | _client = new TestServer(webHost).CreateClient();
52 | }
53 |
54 | [Fact]
55 | public void Returns_correct_response_type()
56 | {
57 | _response.Content.Headers.ContentType.MediaType.ShouldBe("application/json");
58 | }
59 |
60 | [Fact]
61 | public void Returns_correct_status_code()
62 | {
63 | _response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
64 | }
65 |
66 | [Fact]
67 | public async Task Returns_correct_body()
68 | {
69 | var content = await _response.Content.ReadAsStringAsync();
70 | content.ShouldContain("{\"message\":\"An exception occured\"}");
71 | }
72 |
73 | public async Task InitializeAsync()
74 | {
75 | _response = await _client.SendAsync(_requestMessage);
76 | }
77 |
78 | public Task DisposeAsync()
79 | => Task.CompletedTask;
80 | }
81 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/LoggerTests/LogExceptionTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 | using System.Threading.Tasks;
4 | using GlobalExceptionHandler.Tests.Fixtures;
5 | using GlobalExceptionHandler.WebApi;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.AspNetCore.TestHost;
10 | using Shouldly;
11 | using Xunit;
12 |
13 | namespace GlobalExceptionHandler.Tests.WebApi.LoggerTests
14 | {
15 | public class LogExceptionTests : IClassFixture, IAsyncLifetime
16 | {
17 | private Exception _exception;
18 | private string _contextType;
19 | private HandlerContext _handlerContext;
20 | private readonly TestServer _server;
21 | private int _statusCode;
22 | private const string RequestUri = "/api/productnotfound";
23 |
24 | public LogExceptionTests(WebApiServerFixture fixture)
25 | {
26 | // Arrange
27 | var webHost = fixture.CreateWebHostWithMvc();
28 | webHost.Configure(app =>
29 | {
30 | app.UseGlobalExceptionHandler(x =>
31 | {
32 | x.OnException((context, _) =>
33 | {
34 | _exception = context.Exception;
35 | _contextType = context.HttpContext.GetType().ToString();
36 | return Task.CompletedTask;
37 | });
38 | x.Map().ToStatusCode(StatusCodes.Status404NotFound).WithBody(
39 | (e, c, h) =>
40 | {
41 | _statusCode = c.Response.StatusCode;
42 | _handlerContext = h;
43 | return Task.CompletedTask;
44 | });
45 | });
46 |
47 | app.Map(RequestUri, config =>
48 | {
49 | config.Run(context => throw new ArgumentException("Invalid request"));
50 | });
51 | });
52 |
53 | _server = new TestServer(webHost);
54 | }
55 |
56 | public async Task InitializeAsync()
57 | {
58 | using (var client = _server.CreateClient())
59 | {
60 | var requestMessage = new HttpRequestMessage(new HttpMethod("GET"), RequestUri);
61 | await client.SendAsync(requestMessage);
62 | }
63 | }
64 |
65 | [Fact]
66 | public void Invoke_logger()
67 | {
68 | _exception.ShouldBeOfType();
69 | }
70 |
71 | [Fact]
72 | public void HttpContext_is_set()
73 | {
74 | _contextType.ShouldBe("Microsoft.AspNetCore.Http.DefaultHttpContext");
75 | }
76 |
77 | [Fact]
78 | public void Handler_context_is_set()
79 | {
80 | _handlerContext.ShouldBeOfType();
81 | }
82 |
83 | [Fact]
84 | public void Status_code_is_set()
85 | {
86 | _statusCode.ShouldBe(StatusCodes.Status404NotFound);
87 | }
88 |
89 | public Task DisposeAsync()
90 | => Task.CompletedTask;
91 | }
92 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.sln.docstates
8 | .vs
9 | .vscode
10 | .idea
11 |
12 | # Build results
13 |
14 | [Dd]ebug/
15 | [Rr]elease/
16 | x64/
17 | build/
18 | package/
19 | [Bb]in/
20 | [Oo]bj/
21 | #dist/
22 | artifacts/
23 |
24 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets
25 | !packages/*/build/
26 |
27 | # MSTest test Results
28 | [Tt]est[Rr]esult*/
29 | [Bb]uild[Ll]og.*
30 |
31 | *_i.c
32 | *_p.c
33 | *.ilk
34 | *.meta
35 | *.obj
36 | *.pch
37 | *.pdb
38 | *.pgc
39 | *.pgd
40 | *.rsp
41 | *.sbr
42 | *.tlb
43 | *.tli
44 | *.tlh
45 | *.tmp
46 | *.tmp_proj
47 | *.log
48 | *.vspscc
49 | *.vssscc
50 | .builds
51 | *.pidb
52 | *.log
53 | *.scc
54 |
55 | # Visual C++ cache files
56 | ipch/
57 | *.aps
58 | *.ncb
59 | *.opensdf
60 | *.sdf
61 | *.cachefile
62 |
63 | # Visual Studio profiler
64 | *.psess
65 | *.vsp
66 | *.vspx
67 |
68 | # Guidance Automation Toolkit
69 | *.gpState
70 |
71 | # ReSharper is a .NET coding add-in
72 | _ReSharper*/
73 | *.[Rr]e[Ss]harper
74 |
75 | # TeamCity is a build add-in
76 | _TeamCity*
77 |
78 | # DotCover is a Code Coverage Tool
79 | *.dotCover
80 |
81 | # NCrunch
82 | *.ncrunch*
83 | .*crunch*.local.xml
84 |
85 | # Installshield output folder
86 | [Ee]xpress/
87 |
88 | # DocProject is a documentation generator add-in
89 | DocProject/buildhelp/
90 | DocProject/Help/*.HxT
91 | DocProject/Help/*.HxC
92 | DocProject/Help/*.hhc
93 | DocProject/Help/*.hhk
94 | DocProject/Help/*.hhp
95 | DocProject/Help/Html2
96 | DocProject/Help/html
97 |
98 | # Click-Once directory
99 | publish/
100 |
101 | # Publish Web Output
102 | *.Publish.xml
103 |
104 | # NuGet Packages Directory
105 | packages/
106 | !packages/repositories.config
107 |
108 | # Windows Azure Build Output
109 | csx
110 | *.build.csdef
111 |
112 | # Windows Store app package directory
113 | AppPackages/
114 |
115 | # Others
116 | sql/
117 | *.Cache
118 | ClientBin/
119 | [Ss]tyle[Cc]op.*
120 | ~$*
121 | *~
122 | *.dbmdl
123 | *.[Pp]ublish.xml
124 | *.pfx
125 | *.publishsettings
126 | node_modules/
127 | bower_components/
128 | tmp/
129 |
130 | # RIA/Silverlight projects
131 | Generated_Code/
132 |
133 | # Backup & report files from converting an old project file to a newer
134 | # Visual Studio version. Backup files are not needed, because we have git ;-)
135 | _UpgradeReport_Files/
136 | Backup*/
137 | UpgradeLog*.XML
138 | UpgradeLog*.htm
139 |
140 | # SQL Server files
141 | App_Data/*.mdf
142 | App_Data/*.ldf
143 |
144 |
145 | #LightSwitch generated files
146 | GeneratedArtifacts/
147 | _Pvt_Extensions/
148 | ModelManifest.xml
149 |
150 | # =========================
151 | # Windows detritus
152 | # =========================
153 |
154 | # Windows image file caches
155 | Thumbs.db
156 | ehthumbs.db
157 |
158 | # Folder config file
159 | Desktop.ini
160 |
161 | # Recycle Bin used on file shares
162 | $RECYCLE.BIN/
163 |
164 | # Mac desktop service store files
165 | .DS_Store
166 |
167 | bower_components/
168 | node_modules/
169 | **/wwwroot/lib/
170 | **/wwwroot/img/media/
171 | .settings
172 | **/PublishProfiles/
173 | **/PublishScripts/
174 |
175 | # Angular 2
176 | # compiled output
177 | AdminApp/dist
178 | AdminApp/tmp
179 |
180 | # dependencies
181 | AdminApp/node_modules
182 | AdminApp/bower_components
183 |
184 | # IDEs and editors
185 | AdminApp/.idea
186 |
187 | # misc
188 | AdminApp/.sass-cache
189 | AdminApp/connect.lock
190 | AdminApp/coverage/*
191 | AdminApp/libpeerconnection.log
192 | AdminApp/AdminApp/npm-debug.log
193 | AdminApp/testem.log
194 | AdminApp/typings
195 |
196 | # e2e
197 | AdminApp/e2e/*.js
198 | AdminApp/e2e/*.map
199 |
200 | #System Files
201 | .DS_Store
202 | Thumbs.db
203 |
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler/WebApi/RuleCreation/ExceptionRuleCreator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Http;
6 |
7 | namespace GlobalExceptionHandler.WebApi
8 | {
9 | public interface IHasStatusCode where TException: Exception
10 | {
11 | IHandledFormatters ToStatusCode(int statusCode);
12 | IHandledFormatters ToStatusCode(HttpStatusCode statusCode);
13 | IHandledFormatters ToStatusCode(Func statusCodeResolver);
14 | IHandledFormatters ToStatusCode(Func statusCodeResolver);
15 | }
16 |
17 | internal class ExceptionRuleCreator : IHasStatusCode, IHandledFormatters where TException: Exception
18 | {
19 | private readonly IDictionary _configurations;
20 |
21 | public ExceptionRuleCreator(IDictionary configurations)
22 | {
23 | _configurations = configurations;
24 | }
25 |
26 | public IHandledFormatters ToStatusCode(int statusCode)
27 | => ToStatusCodeImpl(ex => statusCode);
28 |
29 | public IHandledFormatters ToStatusCode(HttpStatusCode statusCode)
30 | => ToStatusCodeImpl(ex => (int)statusCode);
31 |
32 | public IHandledFormatters ToStatusCode(Func statusCodeResolver)
33 | => ToStatusCodeImpl(statusCodeResolver);
34 |
35 | public IHandledFormatters ToStatusCode(Func statusCodeResolver)
36 | => ToStatusCodeImpl(x => (int)statusCodeResolver(x));
37 |
38 | private IHandledFormatters ToStatusCodeImpl(Func statusCodeResolver)
39 | {
40 | int WrappedResolver(Exception x) => statusCodeResolver((TException)x);
41 | var exceptionConfig = new ExceptionConfig
42 | {
43 | StatusCodeResolver = WrappedResolver
44 | };
45 |
46 | _configurations.Add(typeof(TException), exceptionConfig);
47 |
48 | return this;
49 | }
50 |
51 | public void WithBody(Func formatter)
52 | {
53 | Task Formatter(TException x, HttpContext y, HandlerContext b)
54 | {
55 | var s = formatter.Invoke(x, y);
56 | return y.Response.WriteAsync(s);
57 | }
58 |
59 | UsingMessageFormatter(Formatter);
60 | }
61 |
62 | public void WithBody(Func formatter)
63 | {
64 | if (formatter == null)
65 | throw new NullReferenceException(nameof(formatter));
66 |
67 | Task Formatter(TException x, HttpContext y, HandlerContext b)
68 | => formatter.Invoke(x, y);
69 |
70 | UsingMessageFormatter(Formatter);
71 | }
72 |
73 | public void UsingMessageFormatter(Func formatter)
74 | => WithBody(formatter);
75 |
76 |
77 | public void WithBody(Func formatter)
78 | => SetMessageFormatter(formatter);
79 |
80 | private void SetMessageFormatter(Func formatter)
81 | {
82 | if (formatter == null)
83 | throw new NullReferenceException(nameof(formatter));
84 |
85 | Task WrappedFormatter(Exception x, HttpContext y, HandlerContext z) => formatter((TException)x, y, z);
86 | var exceptionConfig = _configurations[typeof(TException)];
87 | exceptionConfig.Formatter = WrappedFormatter;
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.Tests/WebApi/MessageFormatterTests/TypeTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Net.Http.Headers;
5 | using System.Threading.Tasks;
6 | using GlobalExceptionHandler.ContentNegotiation;
7 | using GlobalExceptionHandler.Tests.Fixtures;
8 | using GlobalExceptionHandler.WebApi;
9 | using Microsoft.AspNetCore.Builder;
10 | using Microsoft.AspNetCore.Hosting;
11 | using Microsoft.AspNetCore.Http;
12 | using Microsoft.AspNetCore.TestHost;
13 | using Shouldly;
14 | using Xunit;
15 |
16 | namespace GlobalExceptionHandler.Tests.WebApi.MessageFormatterTests
17 | {
18 | public class TypeTests : IClassFixture, IAsyncLifetime
19 | {
20 | private const string ApiProductNotFound = "/api/productnotfound";
21 | private readonly HttpClient _client;
22 | private readonly HttpRequestMessage _requestMessage;
23 | private HttpResponseMessage _response;
24 |
25 | public TypeTests(WebApiServerFixture fixture)
26 | {
27 | // Arrange
28 | var webHost = fixture.CreateWebHostWithXmlFormatters();
29 | webHost.Configure(app =>
30 | {
31 | app.UseGlobalExceptionHandler(x =>
32 | {
33 | x.Map()
34 | .ToStatusCode(StatusCodes.Status502BadGateway)
35 | .WithBody((e, c, h) => c.Response.WriteAsync("Not Thrown Message"));
36 |
37 | x.Map()
38 | .ToStatusCode(StatusCodes.Status409Conflict)
39 | .WithBody(new TestResponse
40 | {
41 | Message = "Conflict"
42 | });
43 |
44 | x.Map()
45 | .ToStatusCode(StatusCodes.Status400BadRequest)
46 | .WithBody(e => new TestResponse
47 | {
48 | Message = "Bad Request"
49 | });
50 |
51 | x.Map()
52 | .ToStatusCode(StatusCodes.Status403Forbidden)
53 | .WithBody(new TestResponse
54 | {
55 | Message = "Forbidden"
56 | });
57 | });
58 |
59 | app.Map(ApiProductNotFound, config =>
60 | {
61 | config.Run(context => throw new Level1ExceptionB());
62 | });
63 | });
64 |
65 | _requestMessage = new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound);
66 | _requestMessage.Headers.Accept.Clear();
67 | _requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml"));
68 |
69 | _client = new TestServer(webHost).CreateClient();
70 | }
71 |
72 | public async Task InitializeAsync()
73 | {
74 | _response = await _client.SendAsync(_requestMessage);
75 | }
76 |
77 | [Fact]
78 | public void Returns_correct_response_type()
79 | {
80 | _response.Content.Headers.ContentType.MediaType.ShouldBe("text/xml");
81 | }
82 |
83 | [Fact]
84 | public void Returns_correct_status_code()
85 | {
86 | _response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
87 | }
88 |
89 | [Fact]
90 | public async Task Returns_correct_body()
91 | {
92 | var content = await _response.Content.ReadAsStringAsync();
93 | content.ShouldContain(@"Bad Request");
94 | }
95 |
96 | public Task DisposeAsync()
97 | => Task.CompletedTask;
98 | }
99 |
100 | internal class BaseException : Exception { }
101 |
102 | internal class Level1ExceptionA : BaseException { }
103 |
104 | internal class Level1ExceptionB : BaseException { }
105 |
106 | internal class Level2ExceptionA : Level1ExceptionA { }
107 |
108 | internal class Level2ExceptionB : Level1ExceptionB { }
109 | }
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27130.2010
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlobalExceptionHandler.Tests", "GlobalExceptionHandler.Tests\GlobalExceptionHandler.Tests.csproj", "{A100852A-D729-4DEB-83F1-13539AFEA8B3}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GlobalExceptionHandler", "GlobalExceptionHandler\GlobalExceptionHandler.csproj", "{6316C52E-85F4-4DFB-AC4B-77EA07E3F605}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GlobalExceptionHandler.Demo", "GlobalExceptionHandler.Demo\GlobalExceptionHandler.Demo.csproj", "{158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Debug|x64 = Debug|x64
16 | Debug|x86 = Debug|x86
17 | Release|Any CPU = Release|Any CPU
18 | Release|x64 = Release|x64
19 | Release|x86 = Release|x86
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Debug|x64.ActiveCfg = Debug|Any CPU
25 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Debug|x64.Build.0 = Debug|Any CPU
26 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Debug|x86.ActiveCfg = Debug|Any CPU
27 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Debug|x86.Build.0 = Debug|Any CPU
28 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Release|x64.ActiveCfg = Release|Any CPU
31 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Release|x64.Build.0 = Release|Any CPU
32 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Release|x86.ActiveCfg = Release|Any CPU
33 | {A100852A-D729-4DEB-83F1-13539AFEA8B3}.Release|x86.Build.0 = Release|Any CPU
34 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Debug|x64.ActiveCfg = Debug|Any CPU
37 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Debug|x64.Build.0 = Debug|Any CPU
38 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Debug|x86.ActiveCfg = Debug|Any CPU
39 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Debug|x86.Build.0 = Debug|Any CPU
40 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Release|x64.ActiveCfg = Release|Any CPU
43 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Release|x64.Build.0 = Release|Any CPU
44 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Release|x86.ActiveCfg = Release|Any CPU
45 | {6316C52E-85F4-4DFB-AC4B-77EA07E3F605}.Release|x86.Build.0 = Release|Any CPU
46 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Debug|x64.ActiveCfg = Debug|Any CPU
49 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Debug|x64.Build.0 = Debug|Any CPU
50 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Debug|x86.ActiveCfg = Debug|Any CPU
51 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Debug|x86.Build.0 = Debug|Any CPU
52 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Release|Any CPU.Build.0 = Release|Any CPU
54 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Release|x64.ActiveCfg = Release|Any CPU
55 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Release|x64.Build.0 = Release|Any CPU
56 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Release|x86.ActiveCfg = Release|Any CPU
57 | {158F5A4A-DC42-4069-84C2-7A0F6D5AEFF2}.Release|x86.Build.0 = Release|Any CPU
58 | EndGlobalSection
59 | GlobalSection(SolutionProperties) = preSolution
60 | HideSolutionNode = FALSE
61 | EndGlobalSection
62 | GlobalSection(ExtensibilityGlobals) = postSolution
63 | SolutionGuid = {A262DDCA-35C7-4A04-B498-93EE323C8DB0}
64 | EndGlobalSection
65 | EndGlobal
66 |
--------------------------------------------------------------------------------
/src/GlobalExceptionHandler/WebApi/ExceptionHandlerConfiguration.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using GlobalExceptionHandler.ContentNegotiation;
6 | using Microsoft.AspNetCore.Diagnostics;
7 | using Microsoft.AspNetCore.Http;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace GlobalExceptionHandler.WebApi
11 | {
12 | public class ExceptionHandlerConfiguration : IUnhandledFormatters
13 | {
14 | private readonly ILogger _logger;
15 | private Type[] _exceptionConfigurationTypesSortedByDepthDescending;
16 | private Func _onException;
17 |
18 | private Func CustomFormatter { get; set; }
19 | private Func DefaultFormatter { get; }
20 | private IDictionary ExceptionConfiguration { get; } = new Dictionary();
21 |
22 | public string ContentType { get; set; }
23 | public int DefaultStatusCode { get; set; } = StatusCodes.Status500InternalServerError;
24 | public bool DebugMode { get; set; }
25 |
26 | public ExceptionHandlerConfiguration(Func defaultFormatter, ILoggerFactory loggerFactory)
27 | {
28 | _logger = loggerFactory.CreateLogger("GlobalExceptionHandlerMiddleware");
29 | DefaultFormatter = defaultFormatter;
30 | }
31 |
32 | public IHasStatusCode Map() where TException : Exception
33 | => new ExceptionRuleCreator(ExceptionConfiguration);
34 |
35 | public void ResponseBody(Func formatter)
36 | {
37 | Task Formatter(Exception exception, HttpContext context, HandlerContext _)
38 | {
39 | var response = formatter.Invoke(exception);
40 | return context.Response.WriteAsync(response);
41 | }
42 |
43 | ResponseBody(Formatter);
44 | }
45 |
46 | public void ResponseBody(Func formatter)
47 | {
48 | Task Formatter(Exception exception, HttpContext context, HandlerContext _)
49 | => formatter.Invoke(exception, context);
50 |
51 | ResponseBody(Formatter);
52 | }
53 |
54 | public void ResponseBody(Func formatter)
55 | {
56 | Task Formatter(Exception exception, HttpContext context, HandlerContext _)
57 | {
58 | var response = formatter.Invoke(exception, context);
59 | return context.Response.WriteAsync(response);
60 | }
61 |
62 | ResponseBody(Formatter);
63 | }
64 |
65 | public void ResponseBody(Func formatter)
66 | => CustomFormatter = formatter;
67 |
68 | // Content negotiation
69 | public void ResponseBody(Func formatter) where T : class
70 | {
71 | Task Formatter(Exception exception, HttpContext context, HandlerContext _)
72 | {
73 | context.Response.ContentType = null;
74 | context.WriteAsyncObject(formatter(exception));
75 | return Task.CompletedTask;
76 | }
77 |
78 | CustomFormatter = Formatter;
79 | }
80 |
81 | [Obsolete("This method has been deprecated, please use to OnException(...) instead", true)]
82 | public void OnError(Func log)
83 | => throw new NotImplementedException();
84 |
85 | public void OnException(Func log)
86 | => _onException = log;
87 |
88 | internal RequestDelegate BuildHandler()
89 | {
90 | var handlerContext = new HandlerContext {ContentType = ContentType};
91 | var exceptionContext = new ExceptionContext();
92 |
93 | _exceptionConfigurationTypesSortedByDepthDescending = ExceptionConfiguration.Keys
94 | .OrderByDescending(x => x, new ExceptionTypePolymorphicComparer())
95 | .ToArray();
96 |
97 | return async context =>
98 | {
99 | var handlerFeature = context.Features.Get();
100 | var exception = handlerFeature.Error;
101 |
102 | if (ContentType != null)
103 | context.Response.ContentType = ContentType;
104 |
105 | // If any custom exceptions are set
106 | foreach (var type in _exceptionConfigurationTypesSortedByDepthDescending)
107 | {
108 | // ReSharper disable once UseMethodIsInstanceOfType TODO: Fire those guys
109 | if (!type.IsAssignableFrom(exception.GetType()))
110 | continue;
111 |
112 | var config = ExceptionConfiguration[type];
113 | context.Response.StatusCode = config.StatusCodeResolver?.Invoke(exception) ?? DefaultStatusCode;
114 |
115 | if (config.Formatter == null)
116 | config.Formatter = CustomFormatter;
117 |
118 | if (_onException != null)
119 | {
120 | exceptionContext.Exception = handlerFeature.Error;
121 | exceptionContext.HttpContext = context;
122 | exceptionContext.ExceptionMatched = type;
123 | await _onException(exceptionContext, _logger);
124 | }
125 |
126 | await config.Formatter(exception, context, handlerContext);
127 | return;
128 | }
129 |
130 | // Global default format output
131 | if (CustomFormatter != null)
132 | {
133 | if (context.Response.HasStarted)
134 | {
135 | if (_onException != null)
136 | {
137 | await _onException(exceptionContext, _logger);
138 | }
139 | _logger.LogError("The response has already started, the exception handler will not be executed.");
140 | return;
141 | }
142 |
143 | context.Response.StatusCode = DefaultStatusCode;
144 | await CustomFormatter(exception, context, handlerContext);
145 |
146 | if (_onException != null)
147 | {
148 | exceptionContext.Exception = handlerFeature.Error;
149 | await _onException(exceptionContext, _logger);
150 | }
151 |
152 | return;
153 | }
154 |
155 | if (_onException != null)
156 | {
157 | exceptionContext.Exception = handlerFeature.Error;
158 | exceptionContext.ExceptionMatched = null;
159 | await _onException(exceptionContext, _logger);
160 | }
161 |
162 | if (DebugMode)
163 | await DefaultFormatter(exception, context, handlerContext);
164 | };
165 | }
166 | }
167 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Global Exception Handling for ASP.NET Core
2 |
3 | [](https://ci.appveyor.com/project/JoeMighty/globalexceptionhandlerdotnet)
4 |
5 | GlobalExceptionHandler.NET allows you to configure application level exception handling as a convention within your ASP.NET Core application, opposed to explicitly handling exceptions within each controller action.
6 |
7 | Configuring your error handling this way reaps the following benefits:
8 |
9 | - Centralised location for handling errors
10 | - Reduce boilerplate try-catch logic in your controllers
11 | - Catch and appropriately handle exceptions outside of the ASP.NET Core framework
12 | - You don't want error codes being visible by consuming APIs (for instance, you want to return 500 for every exception)
13 |
14 | This middleware targets the ASP.NET Core pipeline with an optional dependency on the MVC framework for content negotiation if so desired.
15 |
16 | **Note:** GlobalExceptionHandler.NET builds on top of the `app.UseExceptionHandler()` middleware so they cannot be used in tandem. GlobalExceptionHandler.NET turns your exception configuration provided by this library into an ExceptionHandler used within the `UseExceptionHandler` middleware.
17 |
18 | ## Installation
19 |
20 | GlobalExceptionHandler is [available on NuGet](https://www.nuget.org/packages/GlobalExceptionHandler/) and can be installed via the below commands depending on your platform:
21 |
22 | ```
23 | $ Install-Package GlobalExceptionHandler
24 | ```
25 | or via the .NET Core CLI:
26 |
27 | ```
28 | $ dotnet add package GlobalExceptionHandler
29 | ```
30 |
31 | ## Bare Bones Setup
32 |
33 | ```csharp
34 | // Startup.cs
35 |
36 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
37 | {
38 | // app.UseExceptionHandler(); You no longer need this.
39 | app.UseGlobalExceptionHandler(x => {
40 | x.ContentType = "application/json";
41 | x.ResponseBody(s => JsonConvert.SerializeObject(new
42 | {
43 | Message = "An error occurred whilst processing your request"
44 | }));
45 | });
46 |
47 | app.Map("/error", x => x.Run(y => throw new Exception()));
48 | }
49 | ```
50 |
51 | Any exception thrown by your application will result in the follow response:
52 |
53 | ```http
54 | HTTP/1.1 500 Internal Server Error
55 | Date: Fri, 24 Nov 2017 09:17:05 GMT
56 | Content-Type: application/json
57 | Server: Kestrel
58 | Cache-Control: no-cache
59 | Pragma: no-cache
60 | Transfer-Encoding: chunked
61 | Expires: -1
62 |
63 | {
64 | "Message": "An error occurred whilst processing your request"
65 | }
66 | ```
67 |
68 | ## Handling specific exceptions
69 |
70 | You can explicitly handle exceptions like so:
71 |
72 | ```csharp
73 | app.UseGlobalExceptionHandler(x => {
74 | x.ContentType = "application/json";
75 | x.ResponseBody(s => JsonConvert.SerializeObject(new
76 | {
77 | Message = "An error occurred whilst processing your request"
78 | }));
79 |
80 | x.Map().ToStatusCode(StatusCodes.Status404NotFound);
81 | });
82 | ```
83 |
84 | ```http
85 | HTTP/1.1 404 Not Found
86 | Date: Sat, 25 Nov 2017 01:47:51 GMT
87 | Content-Type: application/json
88 | Server: Kestrel
89 | Cache-Control: no-cache
90 | Pragma: no-cache
91 | Transfer-Encoding: chunked
92 | Expires: -1
93 |
94 | {
95 | "Message": "An error occurred whilst processing your request"
96 | }
97 | ```
98 |
99 | #### Runtime Status Code
100 |
101 | If talking to a remote service, you could optionally choose to forward the status code on, or propagate it via the exception using the following `ToStatusCode(..)` overload:
102 |
103 | ```csharp
104 | app.UseGlobalExceptionHandler(x =>
105 | {
106 | x.ContentType = "application/json";
107 | x.Map().ToStatusCode(ex => ex.StatusCode).WithBody((e, c) => "Resource could not be found");
108 | ...
109 | });
110 | ```
111 |
112 | ### Per exception responses
113 |
114 | Or provide a custom error response for the exception type thrown:
115 |
116 | ```csharp
117 | app.UseGlobalExceptionHandler(x => {
118 | x.ContentType = "application/json";
119 | x.ResponseBody(s => JsonConvert.SerializeObject(new
120 | {
121 | Message = "An error occurred whilst processing your request"
122 | }));
123 |
124 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
125 | .WithBody((ex, context) => JsonConvert.SerializeObject(new {
126 | Message = "Resource could not be found"
127 | }));
128 | });
129 | ```
130 |
131 | Response:
132 |
133 | ```json
134 | HTTP/1.1 404 Not Found
135 | ...
136 | {
137 | "Message": "Resource could not be found"
138 | }
139 | ```
140 |
141 | Alternatively you could output the exception content if you prefer:
142 |
143 | ```csharp
144 | app.UseGlobalExceptionHandler(x => {
145 | x.ContentType = "application/json";
146 | x.ResponseBody(s => JsonConvert.SerializeObject(new
147 | {
148 | Message = "An error occurred whilst processing your request"
149 | }));
150 |
151 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
152 | .WithBody((ex, context) => JsonConvert.SerializeObject(new {
153 | Message = ex.Message
154 | }));
155 | });
156 | ```
157 |
158 | ## Content Negotiation
159 |
160 | GlobalExceptionHandlerDotNet plugs into the .NET Core pipeline, meaning you can also take advantage of content negotiation provided by the ASP.NET Core MVC framework, enabling the clients to dictate the preferred content type.
161 |
162 | To enable content negotiation against ASP.NET Core MVC you will need to include the [GlobalExceptionHandler.ContentNegotiation.Mvc](https://www.nuget.org/packages/GlobalExceptionHandler.ContentNegotiation.Mvc/) package.
163 |
164 | Note: Content negotiation is handled by ASP.NET Core MVC so this takes a dependency on MVC.
165 |
166 | ```csharp
167 | //Startup.cs
168 |
169 | public void ConfigureServices(IServiceCollection services)
170 | {
171 | services.AddMvcCore().AddXmlSerializerFormatters();
172 | }
173 |
174 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
175 | {
176 | app.UseGlobalExceptionHandler(x =>
177 | {
178 | x.Map().ToStatusCode(StatusCodes.Status404NotFound)
179 | .WithBody(e => new ErrorResponse
180 | {
181 | Message = e.Message
182 | });
183 | });
184 |
185 | app.Map("/error", x => x.Run(y => throw new RecordNotFoundException("Resource could not be found")));
186 | }
187 | ```
188 |
189 | Now when an exception is thrown and the consumer has provided the `Accept` header:
190 |
191 | ```http
192 | GET /api/demo HTTP/1.1
193 | Host: localhost:5000
194 | Accept: text/xml
195 | ```
196 |
197 | The response will be formatted according to the `Accept` header value:
198 |
199 | ```http
200 | HTTP/1.1 404 Not Found
201 | Date: Tue, 05 Dec 2017 08:49:07 GMT
202 | Content-Type: text/xml; charset=utf-8
203 | Server: Kestrel
204 | Cache-Control: no-cache
205 | Pragma: no-cache
206 | Transfer-Encoding: chunked
207 | Expires: -1
208 |
209 |
212 | Resource could not be found
213 |
214 | ```
215 |
216 | ## Logging
217 |
218 | Under most circumstances you'll want to keep a log of any exceptions thrown in your log aggregator of choice. You can do this via the `OnError` endpoint:
219 |
220 | ```csharp
221 | x.OnError((exception, httpContext) =>
222 | {
223 | _logger.Error(exception.Message);
224 | return Task.CompletedTask;
225 | });
226 | ```
227 |
228 | ## Configuration Options:
229 |
230 | - `ContentType`
231 | Specify the returned content type (default is `application/json)`.
232 |
233 | - `ResponseBody(...)`
234 | Set a default response body that any unhandled exception will trigger.
235 |
236 | ```csharp
237 | x.ResponseBody((ex, context) => {
238 | return "Oops, something went wrong! Check the logs for more information.";
239 | });
240 | ```
241 |
242 | - `DebugMode`
243 | Enabling debug mode will cause GlobalExceptionHandlerDotNet to return the full exception thrown. **This is disabled by default and should not be set in production.**
244 |
--------------------------------------------------------------------------------