├── docs ├── CNAME └── index.html ├── assets ├── logo.psd └── logo_128_128.png ├── package.sh ├── src ├── GlobalExceptionHandler.Demo │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── Controllers │ │ └── ValuesController.cs │ ├── RecordNotFoundException.cs │ ├── Program.cs │ ├── GlobalExceptionHandler.Demo.csproj │ ├── Properties │ │ └── launchSettings.json │ └── Startup.cs ├── GlobalExceptionHandler.Tests │ ├── TestResponse.cs │ ├── Infrastructure │ │ └── TestServerBuilder.cs │ ├── Exceptions │ │ ├── NeverThrownException.cs │ │ ├── RecordNotFoundException.cs │ │ └── HttpNotFoundException.cs │ ├── GlobalExceptionHandler.Tests.csproj │ ├── Fixtures │ │ └── WebApiServerFixture.cs │ └── WebApi │ │ ├── GlobalFormatterTests │ │ ├── DebugModeDisabledTests.cs │ │ ├── DebugModeEnabledTests.cs │ │ ├── BareMetalTests.cs │ │ ├── BasicTests.cs │ │ ├── FallBackResponseTest.cs │ │ ├── OverrideFallbackResponseTest.cs │ │ └── BasicWithOverrideTests.cs │ │ ├── MessageFormatterTests │ │ ├── StringMessageFormatter.cs │ │ └── TypeTests.cs │ │ ├── StatusCodeTests │ │ ├── StatusCodeWithInt.cs │ │ ├── StatusCodeAtRunTimeWithEnum.cs │ │ ├── StatusCodeWithEnum.cs │ │ └── StatusCodeAtRunTimeWithInt.cs │ │ ├── LoggerTests │ │ ├── HandledExceptionLoggerTests.cs │ │ ├── UnhandledExceptionLoggerTests.cs │ │ ├── UnhandledExceptionTests.cs │ │ └── LogExceptionTests.cs │ │ └── ContentNegotiationTests │ │ ├── CustomFormatter │ │ ├── PlainTextResponse.cs │ │ ├── XmlResponse.cs │ │ ├── XmlResponseWithException.cs │ │ ├── JsonResponse.cs │ │ └── JsonResponseWithException.cs │ │ └── GlobalFormatter │ │ ├── XmlResponse.cs │ │ └── JsonResponse.cs ├── GlobalExceptionHandler │ ├── WebApi │ │ ├── ExceptionHandler.cs │ │ ├── HandlerContext.cs │ │ ├── ExceptionConfig.cs │ │ ├── ExceptionHandlerOptionsExtensions.cs │ │ ├── ExceptionTypePolymorphicComparer.cs │ │ ├── IFormatters.cs │ │ ├── WebApiExceptionHandlingExtension.cs │ │ ├── RuleCreation │ │ │ └── ExceptionRuleCreator.cs │ │ └── ExceptionHandlerConfiguration.cs │ ├── ContentNegotiation │ │ ├── EmptyActionContext.cs │ │ ├── HttpContextExtensions.cs │ │ ├── ResponseBodyMessageFormatter.cs │ │ └── MessageFormatter.cs │ └── GlobalExceptionHandler.csproj └── GlobalExceptionHandler.sln ├── appveyor.yml ├── LICENSE ├── .gitignore └── README.md /docs/CNAME: -------------------------------------------------------------------------------- 1 | globalexceptionhandler.net -------------------------------------------------------------------------------- /assets/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephwoodward/GlobalExceptionHandlerDotNet/master/assets/logo.psd -------------------------------------------------------------------------------- /assets/logo_128_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/josephwoodward/GlobalExceptionHandlerDotNet/master/assets/logo_128_128.png -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf ./package 4 | dotnet pack ./src/GlobalExceptionHandler/GlobalExceptionHandler.csproj -o ./package/ -c release 5 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/TestResponse.cs: -------------------------------------------------------------------------------- 1 | namespace GlobalExceptionHandler.Tests 2 | { 3 | public class TestResponse 4 | { 5 | public string Message { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/Infrastructure/TestServerBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace GlobalExceptionHandler.Tests.Infrastructure 2 | { 3 | public class TestServerBuilder 4 | { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/WebApi/ExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | namespace GlobalExceptionHandler.WebApi 2 | { 3 | public interface IGlobalExceptionHandler 4 | { 5 | void OnException(); 6 | } 7 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/Controllers/ValuesController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace GlobalExceptionHandler.Demo.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | [ApiController] 8 | public class ValuesController : ControllerBase 9 | { 10 | [HttpGet] 11 | public ActionResult> Get() 12 | => throw new RecordNotFoundException(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/RecordNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GlobalExceptionHandler.Demo 4 | { 5 | public class RecordNotFoundException : Exception 6 | { 7 | public RecordNotFoundException() 8 | { 9 | } 10 | 11 | public RecordNotFoundException(string message) : base(message) 12 | { 13 | } 14 | 15 | public RecordNotFoundException(string message, Exception inner) : base(message, inner) 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/ContentNegotiation/EmptyActionContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Abstractions; 4 | using Microsoft.AspNetCore.Routing; 5 | 6 | namespace GlobalExceptionHandler.ContentNegotiation 7 | { 8 | internal class EmptyActionContext : ActionContext 9 | { 10 | public EmptyActionContext(HttpContext httpContext) : base(httpContext, new RouteData(), new ActionDescriptor()) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/Exceptions/NeverThrownException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GlobalExceptionHandler.Tests.Exceptions 4 | { 5 | public class NeverThrownException : Exception 6 | { 7 | public NeverThrownException() 8 | { 9 | } 10 | 11 | public NeverThrownException(string message) : base(message) 12 | { 13 | } 14 | 15 | public NeverThrownException(string message, Exception inner) : base(message, inner) 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace GlobalExceptionHandler.Demo 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/Exceptions/RecordNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GlobalExceptionHandler.Tests.Exceptions 4 | { 5 | public class RecordNotFoundException : Exception 6 | { 7 | public RecordNotFoundException() 8 | { 9 | } 10 | 11 | public RecordNotFoundException(string message) : base(message) 12 | { 13 | } 14 | 15 | public RecordNotFoundException(string message, Exception inner) : base(message, inner) 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/WebApi/HandlerContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace GlobalExceptionHandler.WebApi 6 | { 7 | public class HandlerContext 8 | { 9 | public string ContentType { get; set; } 10 | 11 | public HttpStatusCode StatusCode { get; set; } 12 | } 13 | 14 | public class ExceptionContext 15 | { 16 | public Exception Exception { get; set; } 17 | 18 | public Type ExceptionMatched { get; set; } 19 | 20 | public HttpContext HttpContext { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/GlobalExceptionHandler.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | Exe 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | image: 3 | - Ubuntu 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | environment: 10 | MINVERBUILDMETADATA: build.%APPVEYOR_BUILD_NUMBER% 11 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 12 | NUGET_XMLDOC_MODE: skip 13 | 14 | nuget: 15 | disable_publish_on_pr: true 16 | 17 | 18 | 19 | build_script: 20 | - ps: .\build.ps1 21 | 22 | pull_requests: 23 | do_not_increment_build_number: true 24 | 25 | skip_tags: false 26 | test: off 27 | 28 | artifacts: 29 | - path: .\artifacts\*.nupkg 30 | name: NuGet 31 | 32 | for: 33 | - 34 | matrix: 35 | only: 36 | - image: Ubuntu 37 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/Exceptions/HttpNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace GlobalExceptionHandler.Tests.Exceptions 5 | { 6 | public class HttpNotFoundException : Exception 7 | { 8 | public HttpNotFoundException() 9 | { 10 | } 11 | 12 | public HttpNotFoundException(string message) : base(message) 13 | { 14 | } 15 | 16 | public HttpNotFoundException(string message, Exception inner) : base(message, inner) 17 | { 18 | } 19 | 20 | public HttpStatusCode StatusCodeEnum 21 | => HttpStatusCode.NotFound; 22 | 23 | public int StatusCodeInt 24 | => 404; 25 | } 26 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/ContentNegotiation/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace GlobalExceptionHandler.ContentNegotiation 6 | { 7 | public static class HttpContextExtensions 8 | { 9 | public static Task WriteAsyncObject(this HttpContext context, object value) 10 | { 11 | // Using content negotiation API so empty content type for MVC pipeline to infer type 12 | context.Response.ContentType = null; 13 | 14 | var emptyActionContext = new EmptyActionContext(context); 15 | var result = new ObjectResult(value); 16 | 17 | return result.ExecuteResultAsync(emptyActionContext); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/WebApi/ExceptionConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace GlobalExceptionHandler.WebApi 6 | { 7 | internal class ExceptionConfig 8 | { 9 | public Func StatusCodeResolver { get; set; } 10 | 11 | public Func Formatter { get; set; } 12 | 13 | public static Task UnsafeFormatterWithDetails(Exception exception, HttpContext httpContext, HandlerContext handlerContext) 14 | => httpContext.Response.WriteAsync(exception.ToString()); 15 | 16 | public static Task SafeFormatterWithDetails(Exception exception, HttpContext httpContext, HandlerContext handlerContext) 17 | => httpContext.Response.WriteAsync("An error occurred while processing your request"); 18 | } 19 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/WebApi/ExceptionHandlerOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace GlobalExceptionHandler.WebApi 6 | { 7 | internal static class ExceptionHandlerOptionsExtensions 8 | { 9 | public static ExceptionHandlerOptions SetHandler(this ExceptionHandlerOptions exceptionHandlerOptions, Action configurationAction, ILoggerFactory loggerFactory) 10 | { 11 | var configuration = new ExceptionHandlerConfiguration(ExceptionConfig.UnsafeFormatterWithDetails, loggerFactory); 12 | configurationAction(configuration); 13 | 14 | exceptionHandlerOptions.ExceptionHandler = configuration.BuildHandler(); 15 | return exceptionHandlerOptions; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/WebApi/ExceptionTypePolymorphicComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GlobalExceptionHandler.WebApi 5 | { 6 | internal class ExceptionTypePolymorphicComparer : IComparer 7 | { 8 | public int Compare(Type x, Type y) 9 | { 10 | var depthOfX = 0; 11 | var currentType = x; 12 | while (currentType != typeof(object)) 13 | { 14 | currentType = currentType.BaseType; 15 | depthOfX++; 16 | } 17 | 18 | var depthOfY = 0; 19 | currentType = y; 20 | while (currentType != typeof(object)) 21 | { 22 | currentType = currentType.BaseType; 23 | depthOfY++; 24 | } 25 | 26 | return depthOfX.CompareTo(depthOfY); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:12844", 8 | "sslPort": 44335 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "GlobalExceptionHandler.Demo": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/values", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/GlobalExceptionHandler.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/WebApi/IFormatters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace GlobalExceptionHandler.WebApi 6 | { 7 | /* Important: Keep these base contract signatures the same for consistency */ 8 | 9 | public interface IHandledFormatters where TException : Exception 10 | { 11 | void WithBody(Func formatter); 12 | 13 | void WithBody(Func formatter); 14 | 15 | void WithBody(Func formatter); 16 | } 17 | 18 | public interface IUnhandledFormatters where TException: Exception 19 | { 20 | void ResponseBody(Func formatter); 21 | 22 | void ResponseBody(Func formatter); 23 | 24 | void ResponseBody(Func formatter); 25 | } 26 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/Fixtures/WebApiServerFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace GlobalExceptionHandler.Tests.Fixtures 7 | { 8 | public class WebApiServerFixture 9 | { 10 | public IWebHostBuilder CreateWebHost() 11 | => CreateWebHost(null); 12 | 13 | public IWebHostBuilder CreateWebHostWithMvc() 14 | => CreateWebHost(s => s.AddMvc()); 15 | 16 | public IWebHostBuilder CreateWebHostWithXmlFormatters() 17 | => CreateWebHost(s => { s.AddMvc().AddXmlSerializerFormatters(); }); 18 | 19 | private static IWebHostBuilder CreateWebHost(Action serviceBuilder) 20 | { 21 | var config = new ConfigurationBuilder().Build(); 22 | var host = new WebHostBuilder() 23 | .UseConfiguration(config); 24 | 25 | if (serviceBuilder != null) 26 | host.ConfigureServices(serviceBuilder); 27 | 28 | return host; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joseph Woodward 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 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/ContentNegotiation/ResponseBodyMessageFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using GlobalExceptionHandler.WebApi; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace GlobalExceptionHandler.ContentNegotiation 7 | { 8 | public class ResponseBodyMessageFormatter 9 | { 10 | public void ResponseBody(Func formatter) 11 | { 12 | Task Formatter(Exception x, HttpContext y, HandlerContext b) 13 | { 14 | var s = formatter.Invoke(x); 15 | return y.Response.WriteAsync(s); 16 | } 17 | 18 | ResponseBody(Formatter); 19 | } 20 | 21 | public void ResponseBody(Func formatter) 22 | { 23 | Task Formatter(Exception x, HttpContext y, HandlerContext b) 24 | => formatter.Invoke(x, y); 25 | 26 | ResponseBody(Formatter); 27 | } 28 | 29 | public void ResponseBody(Func formatter) 30 | { 31 | Task Formatter(Exception x, HttpContext y, HandlerContext b) 32 | { 33 | var s = formatter.Invoke(x, y); 34 | return y.Response.WriteAsync(s); 35 | } 36 | 37 | ResponseBody(Formatter); 38 | } 39 | 40 | public void ResponseBody(Func formatter) 41 | { 42 | /* 43 | CustomFormatter = formatter; 44 | */ 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/WebApi/WebApiExceptionHandlingExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Diagnostics; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace GlobalExceptionHandler.WebApi 9 | { 10 | public static class WebApiExceptionHandlingExtensions 11 | { 12 | public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app) 13 | => UseGlobalExceptionHandler(app, configuration => {}); 14 | 15 | public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app, Action configuration) 16 | => app.UseGlobalExceptionHandler(configuration, NullLoggerFactory.Instance); 17 | 18 | public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app, Action configuration, ILoggerFactory loggerFactory) 19 | { 20 | if (app == null) 21 | throw new ArgumentNullException(nameof(app)); 22 | if (configuration == null) 23 | throw new ArgumentNullException(nameof(configuration)); 24 | if (loggerFactory == null) 25 | throw new ArgumentNullException(nameof(loggerFactory)); 26 | 27 | var options = new ExceptionHandlerOptions().SetHandler(configuration, loggerFactory); 28 | return app.UseMiddleware(Options.Create(options), NullLoggerFactory.Instance); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/GlobalExceptionHandler.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | GlobalExceptionHandler 5 | GlobalExceptionHandler 6 | latest 7 | global exception handler;asp.net core;.net core 8 | Global Exception Handling Middleware 9 | 5.0.0 10 | https://github.com/JosephWoodward/GlobalExceptionHandlerDotNet/releases/tag/5.0.0 11 | Joseph Woodward 12 | Global Exception Handler is middleware allowing you to handle exceptions by convention 13 | https://github.com/JosephWoodward/GlobalExceptionHandlerDotNet 14 | https://raw.githubusercontent.com/JosephWoodward/GlobalExceptionHandlerDotNet/master/assets/logo_128_128.png 15 | https://github.com/JosephWoodward/GlobalExceptionHandlerDotNet/blob/master/LICENSE 16 | diag 17 | v 18 | Library 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | all 27 | 28 | 29 | runtime; build; native; contentfiles; analyzers 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Demo/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using GlobalExceptionHandler.WebApi; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Logging.Console; 10 | using Microsoft.Extensions.Options; 11 | using Newtonsoft.Json; 12 | 13 | namespace GlobalExceptionHandler.Demo 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 | 30 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 31 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 32 | { 33 | app.UseGlobalExceptionHandler(x => 34 | { 35 | x.ContentType = "application/json"; 36 | x.ResponseBody(s => JsonConvert.SerializeObject(new {Message = "An error occured whilst processing your request"})); 37 | x.OnException((c, logger) => 38 | { 39 | logger.LogError("Something's gone wrong"); 40 | return Task.CompletedTask; 41 | }); 42 | x.Map().ToStatusCode(StatusCodes.Status404NotFound).WithBody((ex, context) => JsonConvert.SerializeObject(new {Message = "Record could not be found"})); 43 | }); 44 | 45 | app.Map("/error", x => x.Run(y => throw new RecordNotFoundException())); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/WebApi/GlobalFormatterTests/DebugModeDisabledTests.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 Shouldly; 11 | using Xunit; 12 | 13 | namespace GlobalExceptionHandler.Tests.WebApi.GlobalFormatterTests 14 | { 15 | public class DebugModeDisabledTests : IClassFixture, IAsyncLifetime 16 | { 17 | private readonly HttpClient _client; 18 | private HttpResponseMessage _response; 19 | private const string ApiProductNotFound = "/api/productnotfound"; 20 | 21 | public DebugModeDisabledTests(WebApiServerFixture fixture) 22 | { 23 | // Arrange 24 | var webHost = fixture.CreateWebHostWithMvc(); 25 | webHost.Configure(app => 26 | { 27 | app.UseGlobalExceptionHandler(x => 28 | { 29 | x.DebugMode = false; 30 | }); 31 | 32 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new ArgumentException("Invalid request")); }); 33 | }); 34 | 35 | _client = new TestServer(webHost).CreateClient(); 36 | } 37 | 38 | [Fact] 39 | public void Returns_correct_response_type() 40 | { 41 | _response.Content.Headers.ContentType.ShouldBeNull(); 42 | } 43 | 44 | [Fact] 45 | public void Returns_correct_status_code() 46 | { 47 | _response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); 48 | } 49 | 50 | [Fact] 51 | public async Task Returns_correct_body() 52 | { 53 | var content = await _response.Content.ReadAsStringAsync(); 54 | content.ShouldBeEmpty(); 55 | } 56 | 57 | public async Task InitializeAsync() 58 | { 59 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound)); 60 | } 61 | 62 | public Task DisposeAsync() 63 | => Task.CompletedTask; 64 | } 65 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/WebApi/MessageFormatterTests/StringMessageFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using GlobalExceptionHandler.Tests.Exceptions; 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.MessageFormatterTests 14 | { 15 | public class StringMessageFormatter : IClassFixture, IAsyncLifetime 16 | { 17 | private readonly HttpClient _client; 18 | private HttpResponseMessage _response; 19 | private const string Response = "Hello World!"; 20 | private const string ApiProductNotFound = "/api/productnotfound"; 21 | 22 | public StringMessageFormatter(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(StatusCodes.Status404NotFound) 32 | .WithBody((exception, context) => Response); 33 | }); 34 | 35 | app.Map(ApiProductNotFound, config => 36 | { 37 | config.Run(context => throw new RecordNotFoundException("Record could not be found")); 38 | }); 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 Correct_response_message() 51 | { 52 | var content = await _response.Content.ReadAsStringAsync(); 53 | content.ShouldBe(Response); 54 | } 55 | 56 | public Task DisposeAsync() 57 | => Task.CompletedTask; 58 | } 59 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/WebApi/GlobalFormatterTests/DebugModeEnabledTests.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 Shouldly; 11 | using Xunit; 12 | 13 | namespace GlobalExceptionHandler.Tests.WebApi.GlobalFormatterTests 14 | { 15 | public class DebugModeEnabledTests : IClassFixture, IAsyncLifetime 16 | { 17 | private const string ApiProductNotFound = "/api/productnotfound"; 18 | private readonly HttpClient _client; 19 | private HttpResponseMessage _response; 20 | 21 | public DebugModeEnabledTests(WebApiServerFixture fixture) 22 | { 23 | // Arrange 24 | var webHost = fixture.CreateWebHostWithMvc(); 25 | webHost.Configure(app => 26 | { 27 | app.UseGlobalExceptionHandler(x => 28 | { 29 | x.DebugMode = true; 30 | }); 31 | 32 | app.Map(ApiProductNotFound, config => { config.Run(context => throw new ArgumentException("Invalid request")); }); 33 | }); 34 | 35 | _client = new TestServer(webHost).CreateClient(); 36 | } 37 | 38 | [Fact] 39 | public void Returns_correct_response_type() 40 | { 41 | _response.Content.Headers.ContentType.ShouldBeNull(); 42 | } 43 | 44 | [Fact] 45 | public void Returns_correct_status_code() 46 | { 47 | _response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); 48 | } 49 | 50 | [Fact] 51 | public async Task Returns_correct_body() 52 | { 53 | var content = await _response.Content.ReadAsStringAsync(); 54 | content.ShouldContain("System.ArgumentException: Invalid request"); 55 | } 56 | 57 | public async Task InitializeAsync() 58 | { 59 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound)); 60 | } 61 | 62 | public Task DisposeAsync() 63 | => Task.CompletedTask; 64 | } 65 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler/ContentNegotiation/MessageFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using GlobalExceptionHandler.WebApi; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace GlobalExceptionHandler.ContentNegotiation 7 | { 8 | public static class MessageFormatters 9 | { 10 | public static void WithBody(this IHandledFormatters formatter, T response) where TException: Exception 11 | { 12 | Task Formatter(Exception x, HttpContext c, HandlerContext b) 13 | { 14 | c.Response.ContentType = null; 15 | c.WriteAsyncObject(response); 16 | return Task.CompletedTask; 17 | } 18 | 19 | formatter.WithBody(Formatter); 20 | } 21 | 22 | public static void WithBody(this IHandledFormatters formatter, Func f) where TException: Exception 23 | { 24 | Task Formatter(Exception e, HttpContext c, HandlerContext b) 25 | { 26 | c.Response.ContentType = null; 27 | c.WriteAsyncObject(f((TException)e)); 28 | return Task.CompletedTask; 29 | } 30 | 31 | formatter.WithBody(Formatter); 32 | } 33 | 34 | public static void UsingMessageFormatter(this IHandledFormatters formatter, Func f) where TException : Exception 35 | { 36 | Task Formatter(Exception e, HttpContext c, HandlerContext b) 37 | { 38 | c.Response.ContentType = null; 39 | c.WriteAsyncObject(f((TException)e, c)); 40 | return Task.CompletedTask; 41 | } 42 | 43 | formatter.WithBody(Formatter); 44 | } 45 | 46 | public static void UsingMessageFormatter(this IHandledFormatters formatter, Func f) where TException : Exception 47 | { 48 | Task Formatter(Exception e, HttpContext c, HandlerContext b) 49 | { 50 | c.Response.ContentType = null; 51 | c.WriteAsyncObject(f((TException)e, c, b)); 52 | return Task.CompletedTask; 53 | } 54 | 55 | formatter.WithBody(Formatter); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/WebApi/GlobalFormatterTests/BareMetalTests.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 Shouldly; 11 | using Xunit; 12 | 13 | namespace GlobalExceptionHandler.Tests.WebApi.GlobalFormatterTests 14 | { 15 | public class BareMetalTests : IClassFixture, IAsyncLifetime 16 | { 17 | private const string ApiProductNotFound = "/api/productnotfound"; 18 | private readonly HttpClient _client; 19 | private HttpResponseMessage _response; 20 | 21 | public BareMetalTests(WebApiServerFixture fixture) 22 | { 23 | // Arrange 24 | var webHost = fixture.CreateWebHost(); 25 | webHost.Configure(app => 26 | { 27 | app.UseGlobalExceptionHandler(); 28 | 29 | app.Map(ApiProductNotFound, config => 30 | { 31 | config.Run(context => throw new ArgumentException("Invalid request")); 32 | }); 33 | app.Map("/test", config => 34 | { 35 | config.Run(context => Task.FromResult("Working")); 36 | }); 37 | }); 38 | 39 | _client = new TestServer(webHost).CreateClient(); 40 | } 41 | 42 | [Fact] 43 | public void Returns_correct_response_type() 44 | { 45 | _response.Content.Headers.ContentType.ShouldBeNull(); 46 | } 47 | 48 | [Fact] 49 | public void Returns_correct_status_code() 50 | { 51 | _response.StatusCode.ShouldBe(HttpStatusCode.InternalServerError); 52 | } 53 | 54 | [Fact] 55 | public async Task Returns_empty_body() 56 | { 57 | var content = await _response.Content.ReadAsStringAsync(); 58 | content.ShouldBeEmpty(); 59 | } 60 | 61 | public async Task InitializeAsync() 62 | { 63 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound)); 64 | } 65 | 66 | public Task DisposeAsync() 67 | => Task.CompletedTask; 68 | } 69 | } -------------------------------------------------------------------------------- /src/GlobalExceptionHandler.Tests/WebApi/StatusCodeTests/StatusCodeWithInt.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.Http; 10 | using Microsoft.AspNetCore.TestHost; 11 | using Newtonsoft.Json; 12 | using Shouldly; 13 | using Xunit; 14 | 15 | namespace GlobalExceptionHandler.Tests.WebApi.StatusCodeTests 16 | { 17 | public class StatusCodeWithInt : IClassFixture, IAsyncLifetime 18 | { 19 | private const string ApiProductNotFound = "/api/productnotfound"; 20 | private readonly HttpClient _client; 21 | private HttpResponseMessage _response; 22 | 23 | public StatusCodeWithInt(WebApiServerFixture fixture) 24 | { 25 | var webHost = fixture.CreateWebHostWithMvc(); 26 | webHost.Configure(app => 27 | { 28 | app.UseGlobalExceptionHandler(x => 29 | { 30 | x.ContentType = "application/json"; 31 | x.Map().ToStatusCode(StatusCodes.Status400BadRequest); 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 | [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.BadRequest); 54 | } 55 | 56 | public async Task InitializeAsync() 57 | { 58 | _response = await _client.SendAsync(new HttpRequestMessage(new HttpMethod("GET"), ApiProductNotFound)); 59 | } 60 | 61 | public Task DisposeAsync() 62 | => Task.CompletedTask; 63 | } 64 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GlobalExceptionHandler.NET 8 | 9 | 10 | 11 | 48 | 49 | 50 |
51 |
GlobalExceptionHandler.NET
52 | 56 | View on GitHub 57 |
58 | 59 |
60 |

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 | [![Build status](https://ci.appveyor.com/api/projects/status/kdbepiak0m6olxw7?svg=true)](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 | --------------------------------------------------------------------------------