Skip to content

Commit 0bd9641

Browse files
committed
feat: Exception handling is improved
1 parent 6a429e2 commit 0bd9641

7 files changed

Lines changed: 85 additions & 20 deletions

File tree

ProjectTemplates/AspNetCore.WebApi/ReferenceProject/ReferenceProject/Controllers/ProductsController.cs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
using System;
1+
using AutoMapper;
2+
using Microsoft.AspNetCore.Mvc;
3+
using ReferenceProject.Exceptions;
4+
using ReferenceProject.Repo;
5+
using System;
26
using System.Collections.Generic;
37
using System.Linq;
4-
using AutoMapper;
5-
using Microsoft.AspNetCore.Mvc;
68

79
namespace ReferenceProject.Controllers
810
{
911
[Route("[controller]")]
1012
[ApiController]
11-
public class ProductsController : ControllerBase
13+
public class ProductsController: ControllerBase
1214
{
1315
Repo.IProductsRepo ProductsRepo { get; }
1416
IMapper Mapper { get; }
@@ -25,8 +27,15 @@ public ProductsController(Repo.IProductsRepo productsRepo, IMapper mapper)
2527
return ProductsRepo.Get().Select(Mapper.Map<Dto.Product>);
2628
}
2729

28-
[HttpGet("action")]
29-
public void Action()
30+
[HttpGet]
31+
[Route("{id}")]
32+
public Dto.Product GetById(int id)
33+
{
34+
return Mapper.Map<Dto.Product>(ProductsRepo.GetById(id));
35+
}
36+
37+
[HttpGet("ThrowAnException")]
38+
public void ThrowAnException()
3039
{
3140
throw new Exception("Example exception");
3241
}

ProjectTemplates/AspNetCore.WebApi/ReferenceProject/ReferenceProject/Exceptions/DuplicateKeyException.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using System;
22
using System.Runtime.Serialization;
33

4-
namespace ReferenceProject.Repo
4+
namespace ReferenceProject.Exceptions
55
{
66
[Serializable]
77
internal class DuplicateKeyException : Exception
88
{
99
public DuplicateKeyException()
10+
: this("Duplicate key")
1011
{
1112
}
1213

ProjectTemplates/AspNetCore.WebApi/ReferenceProject/ReferenceProject/Middleware/ExceptionMiddleware.cs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,29 @@
55
using Microsoft.Extensions.Logging;
66
using Newtonsoft.Json;
77
using Newtonsoft.Json.Serialization;
8-
using ReferenceProject.Repo;
8+
using Microsoft.AspNetCore.Hosting;
9+
using ReferenceProject.Exceptions;
910

1011
namespace ReferenceProject.Middleware
1112
{
13+
/// <summary>
14+
/// Middleware to handle unhandled exceptions.
15+
/// It separates exceptions based on their type and returns different status codes and answers based on it, instead of 500 Internal Server Error code in all cases
16+
/// </summary>
17+
/// <remarks>
18+
/// There is another way to do this - an exception filter.
19+
/// However, a middleware is a preferred way to achieve this according to the official documentation.
20+
/// To learn more see https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#exception-filters
21+
/// </remarks>
1222
public class ExceptionMiddleware
1323
{
1424
RequestDelegate Next { get; }
15-
public ILogger Logger { get; }
25+
ILogger Logger { get; }
26+
IHostingEnvironment Environment { get; }
1627

17-
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
28+
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostingEnvironment environment)
1829
{
30+
Environment = environment ?? throw new ArgumentNullException(nameof(environment));
1931
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
2032
Next = next ?? throw new ArgumentNullException(nameof(next));
2133
}
@@ -29,6 +41,8 @@ public async Task InvokeAsync(HttpContext context)
2941
}
3042
catch (Exception ex)
3143
{
44+
// If context.Response.HasStarted == true, then we can't write to the response stream anymore. So we have to restore the body.
45+
// If we were don't do that we get an exception.
3246
context.Response.Body = body;
3347
await HandleExceptionAsync(context, ex);
3448
}
@@ -41,6 +55,7 @@ async Task HandleExceptionAsync(HttpContext context, Exception ex)
4155
context.Response.ContentType = "application/json";
4256
context.Response.StatusCode = statusCode;
4357

58+
// We can decide what the status code should be
4459
if (ex is KeyNotFoundException)
4560
{
4661
context.Response.StatusCode = StatusCodes.Status404NotFound;
@@ -52,23 +67,27 @@ async Task HandleExceptionAsync(HttpContext context, Exception ex)
5267

5368
await context.Response.WriteAsync(
5469
JsonConvert.SerializeObject(
55-
new ErrorResponse(ex),
56-
new JsonSerializerSettings()
57-
{
58-
ContractResolver = new CamelCasePropertyNamesContractResolver()
59-
}));
70+
new ErrorResponse(ex, Environment.IsDevelopment())));
6071

6172
if (context.Response.StatusCode == 500)
6273
{
6374
Logger.LogError(ex, "Unhandled exception occurred");
6475
}
76+
else
77+
{
78+
Logger.LogDebug(ex, "Unhandled exception occurred");
79+
}
6580
}
6681

6782
class ErrorResponse
6883
{
69-
public ErrorResponse(Exception ex)
84+
public ErrorResponse(Exception ex, bool includeFullExceptionInfo)
7085
{
7186
Error = new ExceptionDescription(ex);
87+
if(includeFullExceptionInfo)
88+
{
89+
Error.Exception = ex;
90+
}
7291
}
7392

7493
public ExceptionDescription Error { get; set; }
@@ -82,7 +101,7 @@ public ExceptionDescription(Exception ex)
82101
}
83102

84103
public string Message { get; set; }
104+
public Exception Exception { get; set; }
85105
}
86-
87106
}
88107
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Microsoft.AspNetCore.Builder;
2+
3+
namespace ReferenceProject
4+
{
5+
public static class ExceptionMiddlewareExtensions
6+
{
7+
public static IApplicationBuilder UseExceptionHandler(
8+
this IApplicationBuilder builder)
9+
{
10+
return builder.UseMiddleware<Middleware.ExceptionMiddleware>();
11+
}
12+
}
13+
}

ProjectTemplates/AspNetCore.WebApi/ReferenceProject/ReferenceProject/ReferenceProject.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<TargetFramework>netcoreapp2.1</TargetFramework>
55
</PropertyGroup>
66

7+
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
8+
<DefineConstants>DEBUG;TRACE</DefineConstants>
9+
</PropertyGroup>
10+
711
<ItemGroup>
812
<Folder Include="wwwroot\" />
913
</ItemGroup>

ProjectTemplates/AspNetCore.WebApi/ReferenceProject/ReferenceProject/Repo/ProductsRepo.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using AutoMapper;
2+
using ReferenceProject.Exceptions;
23
using ReferenceProject.Model;
34
using System;
45
using System.Collections.Generic;

ProjectTemplates/AspNetCore.WebApi/ReferenceProject/ReferenceProject/Startup.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ public Startup(IConfiguration configuration, IHostingEnvironment env)
2929
{
3030
DotNetEnv.Env.Load();
3131
}
32+
33+
JsonConvert.DefaultSettings = () =>
34+
new JsonSerializerSettings()
35+
{
36+
ContractResolver = new CamelCasePropertyNamesContractResolver(),
37+
NullValueHandling = NullValueHandling.Ignore,
38+
DefaultValueHandling = DefaultValueHandling.Include,
39+
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
40+
//Formatting = Formatting.Indented,
41+
#if DEBUG
42+
Formatting = Formatting.Indented
43+
#else
44+
Formatting = Formatting.None
45+
#endif
46+
};
3247
}
3348

3449
public IConfiguration Configuration { get; }
@@ -44,7 +59,8 @@ public void ConfigureServices(IServiceCollection services)
4459
// Add useful interface for accessing the IUrlHelper outside a controller.
4560
.AddScoped<IUrlHelper>(x => x
4661
.GetRequiredService<IUrlHelperFactory>()
47-
.GetUrlHelper(x.GetRequiredService<IActionContextAccessor>().ActionContext)).AddMvc()
62+
.GetUrlHelper(x.GetRequiredService<IActionContextAccessor>().ActionContext))
63+
.AddMvc()
4864
.AddJsonOptions(options =>
4965
{
5066
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
@@ -91,8 +107,10 @@ public void ConfigureContainerCommon(ContainerBuilder builder)
91107
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
92108
public void Configure(IApplicationBuilder app/*, IHostingEnvironment env*/)
93109
{
94-
app.UseMiddleware<ExceptionMiddleware>()
95-
.UseMiddleware<PreventResponseCachingMiddleware>();
110+
// Use our exception handler middleware before any other handlers
111+
app.UseExceptionHandler();
112+
113+
app.UseMiddleware<PreventResponseCachingMiddleware>();
96114

97115
app.UseCors(builder => builder
98116
.AllowAnyOrigin()

0 commit comments

Comments
 (0)