Skip to content

Commit 7d3cafc

Browse files
committed
Exception handling refactor
1 parent 244c865 commit 7d3cafc

File tree

15 files changed

+237
-105
lines changed

15 files changed

+237
-105
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using Ahk.GradeManagement.Backend.Common.Options;
2+
using Ahk.GradeManagement.Shared.Dtos.ErrorHandling;
3+
4+
using AutSoft.Common.Exceptions;
5+
6+
using Microsoft.AspNetCore.Diagnostics;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.Data.SqlClient;
9+
using Microsoft.EntityFrameworkCore;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Ahk.GradeManagement.Api.ErrorHandling;
13+
14+
public static class ErrorHandlingExtensions
15+
{
16+
public static IServiceCollection AddCustomProblemDetails(this IServiceCollection services, IConfiguration configuration)
17+
{
18+
services.ConfigureOption<ErrorHandlingOptions>(configuration);
19+
20+
return services.AddProblemDetails(o => o.CustomizeProblemDetails = (ctx) =>
21+
{
22+
switch (ctx.HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error)
23+
{
24+
case EntityNotFoundException e:
25+
UpdateProblemDetails(ctx, e, StatusCodes.Status404NotFound, "Entity not found!", detail: e.Message);
26+
break;
27+
case ForbiddenException e:
28+
UpdateProblemDetails(ctx, e, StatusCodes.Status403Forbidden, e.Title);
29+
break;
30+
case ValidationException e:
31+
UpdateValidationProblemDetails(ctx, e, e.Errors, StatusCodes.Status400BadRequest, e.Title, e.Type?.ToString());
32+
break;
33+
case BusinessException e:
34+
UpdateProblemDetails(ctx, e, StatusCodes.Status500InternalServerError, e.Title, e.Type?.ToString(), e.Message);
35+
break;
36+
// Unique constraints
37+
case DbUpdateException e when e.InnerException is SqlException sqlEx && (sqlEx.Number == 2601 || sqlEx.Number == 2627):
38+
UpdateProblemDetails(ctx, e, StatusCodes.Status409Conflict, "Operation would conflicted to another object!");
39+
break;
40+
case NotImplementedException e:
41+
UpdateProblemDetails(ctx, e, StatusCodes.Status501NotImplemented, "Operation is not implemented yet!");
42+
break;
43+
case OperationCanceledException e when ctx.HttpContext.RequestAborted.IsCancellationRequested:
44+
// https://httpstatuses.com/499
45+
UpdateProblemDetails(ctx, e, 499, "Operation was canceled!");
46+
break;
47+
case OperationCanceledException e when !ctx.HttpContext.RequestAborted.IsCancellationRequested:
48+
UpdateProblemDetails(ctx, e, StatusCodes.Status504GatewayTimeout, "Operation has benn timed out!");
49+
break;
50+
case TimeoutException e:
51+
UpdateProblemDetails(ctx, e, StatusCodes.Status504GatewayTimeout, "Operation has benn timed out!");
52+
break;
53+
case HttpRequestException e when e.IsDbTimeout() || e.IsHttpTimeout():
54+
UpdateProblemDetails(ctx, e, StatusCodes.Status504GatewayTimeout, "Operation has benn timed out!");
55+
break;
56+
case Exception e:
57+
UpdateProblemDetails(ctx, e, StatusCodes.Status500InternalServerError, "Error!");
58+
break;
59+
default:
60+
break;
61+
}
62+
});
63+
}
64+
65+
public static void UpdateProblemDetails(
66+
ProblemDetailsContext ctx,
67+
Exception exception,
68+
int? statusCode = null,
69+
string? title = null,
70+
string? type = null,
71+
string? detail = null,
72+
string? instance = null)
73+
{
74+
if (statusCode.HasValue)
75+
{
76+
ctx.HttpContext.Response.StatusCode = statusCode.Value;
77+
ctx.ProblemDetails.Status = statusCode.Value;
78+
}
79+
80+
if (title != null)
81+
{
82+
ctx.ProblemDetails.Title = title;
83+
}
84+
85+
if (type != null)
86+
{
87+
ctx.ProblemDetails.Type = type;
88+
}
89+
90+
if (detail != null)
91+
{
92+
ctx.ProblemDetails.Detail = detail;
93+
}
94+
95+
if (instance != null)
96+
{
97+
ctx.ProblemDetails.Instance = instance;
98+
}
99+
100+
var options = ctx.HttpContext.RequestServices.GetRequiredService<IOptions<ErrorHandlingOptions>>();
101+
102+
if (options.Value.ReturnExceptionDetails)
103+
{
104+
ctx.ProblemDetails.Extensions["exception"] = new ExceptionDetails()
105+
{
106+
Message = exception.Message,
107+
Type = exception.GetType().Name,
108+
StackTrace = exception.StackTrace,
109+
};
110+
}
111+
}
112+
113+
public static void UpdateValidationProblemDetails(
114+
this ProblemDetailsContext context,
115+
Exception exception,
116+
Dictionary<string, string> errors,
117+
int? statusCode = null,
118+
string? title = null,
119+
string? type = null,
120+
string? detail = null,
121+
string? instance = null)
122+
{
123+
UpdateProblemDetails(context, exception, statusCode, title, type, detail, instance);
124+
if (context.ProblemDetails is ValidationProblemDetails validationProblemDetails)
125+
{
126+
foreach (var error in errors)
127+
{
128+
validationProblemDetails.Errors[error.Key] = [error.Value];
129+
}
130+
}
131+
else if (context.ProblemDetails is not null)
132+
{
133+
var problemDetailsErrors = new Dictionary<string, string[]>();
134+
foreach (var error in errors)
135+
{
136+
problemDetailsErrors[error.Key] = [error.Value];
137+
}
138+
139+
context.ProblemDetails.Extensions["errors"] = problemDetailsErrors;
140+
}
141+
}
142+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Ahk.GradeManagement.Api.ErrorHandling;
2+
3+
public class ErrorHandlingOptions
4+
{
5+
public bool ReturnExceptionDetails { get; set; }
6+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Microsoft.Data.SqlClient;
2+
3+
using System.Net.Sockets;
4+
5+
namespace Ahk.GradeManagement.Api.ErrorHandling;
6+
7+
public static class ExceptionExtensions
8+
{
9+
private static readonly HashSet<int> RetriableSqlErrorClasses = [11, 13, 16, 17, 18, 19, 20, 21, 22, 24];
10+
private static readonly HashSet<int> RetriableSqlErrorNumbers = [-2, 1205];
11+
12+
/// <summary>
13+
/// https://stackoverflow.com/a/24041546/1406798
14+
/// </summary>
15+
/// <returns>Returns true if the exception is considered as a retryable timeout</returns>
16+
public static bool IsDbTimeout(this Exception e) =>
17+
e is SqlException sqle
18+
&& (RetriableSqlErrorClasses.Contains(sqle.Class) || RetriableSqlErrorNumbers.Contains(sqle.Number));
19+
20+
public static bool IsHttpTimeout(this Exception ex) =>
21+
ex.InnerException is SocketException socketException && socketException.SocketErrorCode == SocketError.TimedOut;
22+
}

src/Ahk.GradeManagement/Ahk.GradeManagement.Api/Middlewares/ExceptionHandlers/DefaultExceptionHandler.cs

Lines changed: 0 additions & 24 deletions
This file was deleted.

src/Ahk.GradeManagement/Ahk.GradeManagement.Api/Middlewares/ExceptionHandlers/EntityNotFoundExceptionHandler.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Ahk.GradeManagement/Ahk.GradeManagement.Api/Middlewares/ExceptionHandlers/ValidationExceptionHandler.cs

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/Ahk.GradeManagement/Ahk.GradeManagement.Api/Program.cs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using Ahk.GradeManagement.Api.Authorization;
2-
using Ahk.GradeManagement.Api.Middlewares;
3-
using Ahk.GradeManagement.Api.Middlewares.ExceptionHandlers;
2+
using Ahk.GradeManagement.Api.ErrorHandling;
43
using Ahk.GradeManagement.Api.RequestContext;
54
using Ahk.GradeManagement.Bll;
6-
using Ahk.GradeManagement.Bll.Profiles;
75
using Ahk.GradeManagement.Dal;
86

97
using Azure.Identity;
@@ -41,40 +39,28 @@ public static void Main(string[] args)
4139

4240
builder.Services.AddGradeManagementDbContext(builder.Configuration, "DbConnection");
4341

44-
// Add services to the container.
45-
4642
builder.Services.AddControllersWithViews();
4743
builder.Services.AddOpenApiDocument(config =>
4844
{
4945
config.DocumentName = "AHK2.OpenAPI";
5046
config.Title = "AHK Grade Management API";
5147
});
52-
builder.Services.AddRazorPages();
5348
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
5449
builder.Services.AddEndpointsApiExplorer();
5550

56-
builder.Services.AddAutoMapper(typeof(AutoMapperProfile).Assembly);
57-
5851
builder.Services.AddBllServices();
5952

60-
builder.Services.AddExceptionHandler<EntityNotFoundExceptionHandler>();
61-
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
62-
builder.Services.AddExceptionHandler<DefaultExceptionHandler>();
63-
builder.Services.AddProblemDetails();
53+
builder.Services.AddCustomProblemDetails(builder.Configuration);
54+
55+
builder.Services.AddRazorPages();
6456
builder.Services.AddMudServices();
6557
builder.Services.AddMudExtensions();
6658

6759
var app = builder.Build();
6860

69-
70-
// Configure the HTTP request pipeline.
7161
if (app.Environment.IsDevelopment())
7262
{
73-
// Add OpenAPI 3.0 document serving middleware
74-
// Available at: https://localhost:7136/swagger/v1/swagger.json
7563
app.UseOpenApi();
76-
// Add web UIs to interact with the document
77-
// Available at: https://localhost:7136/swagger
7864
app.UseSwaggerUi();
7965
app.UseWebAssemblyDebugging();
8066
}

src/Ahk.GradeManagement/Ahk.GradeManagement.Api/appsettings.Development.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@
77
"Default": "Information",
88
"Microsoft.AspNetCore": "Warning"
99
}
10+
},
11+
"ErrorHandlingOptions": {
12+
"ReturnExceptionDetails": true
1013
}
1114
}

src/Ahk.GradeManagement/Ahk.GradeManagement.Api/appsettings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@
1414
"ClientId": "0ff49368-7e23-4e6a-9c57-973a6cac8bbd",
1515
"Domain": "m365.bme.hu",
1616
"TenantId": "6a3548ab-7570-4271-91a8-58da00697029"
17+
},
18+
"ErrorHandlingOptions": {
19+
"ReturnExceptionDetails": false
1720
}
1821
}

src/Ahk.GradeManagement/Ahk.GradeManagement.Backend.Common/Ahk.GradeManagement.Backend.Common.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@
66
<Nullable>enable</Nullable>
77
</PropertyGroup>
88

9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
11+
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
12+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.7" />
13+
</ItemGroup>
14+
915
</Project>

0 commit comments

Comments
 (0)