Skip to content

Commit d0c3b87

Browse files
authored
Merge pull request #10 from DeviesDevelopment/exception
Add exception handling with tests
2 parents 2edf768 + 1ee061c commit d0c3b87

27 files changed

+703
-136
lines changed

EntityInjector.Route/EntityInjector.Route.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
55
<PackageId>EntityInjector.Route</PackageId>
6-
<Version>1.0.0</Version>
6+
<Version>1.0.1</Version>
77
<Company>Devies</Company>
88
<Authors>John Johansson; Erik Jergéus</Authors>
99
<TargetFramework>netstandard2.1</TargetFramework>
@@ -18,8 +18,9 @@
1818
</PropertyGroup>
1919

2020
<ItemGroup>
21-
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0"/>
22-
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0"/>
21+
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
22+
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0" />
23+
<PackageReference Include="System.Text.Json" Version="9.0.7" />
2324
</ItemGroup>
2425

2526
</Project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace EntityInjector.Route.Exceptions.Middleware;
5+
6+
public class DefaultRouteBindingProblemDetailsFactory : IRouteBindingProblemDetailsFactory
7+
{
8+
public ProblemDetails Create(HttpContext context, RouteBindingException exception)
9+
{
10+
return new ProblemDetails
11+
{
12+
Status = exception.StatusCode,
13+
Detail = exception.Message,
14+
Instance = context.Request.Path
15+
};
16+
}
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace EntityInjector.Route.Exceptions.Middleware;
5+
6+
public interface IRouteBindingProblemDetailsFactory
7+
{
8+
ProblemDetails Create(HttpContext context, RouteBindingException exception);
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Microsoft.AspNetCore.Builder;
2+
3+
namespace EntityInjector.Route.Exceptions.Middleware;
4+
5+
public static class RouteBindingApplicationBuilderExtensions
6+
{
7+
public static IApplicationBuilder UseRouteBinding(this IApplicationBuilder app)
8+
{
9+
return app.UseMiddleware<RouteBindingExceptionMiddleware>();
10+
}
11+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Text.Json;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace EntityInjector.Route.Exceptions.Middleware;
6+
7+
public class RouteBindingExceptionMiddleware(
8+
RequestDelegate next,
9+
ILogger<RouteBindingExceptionMiddleware> logger,
10+
IRouteBindingProblemDetailsFactory? problemDetailsFactory = null)
11+
{
12+
private readonly IRouteBindingProblemDetailsFactory _problemDetailsFactory = problemDetailsFactory ?? new DefaultRouteBindingProblemDetailsFactory();
13+
private static readonly JsonSerializerOptions JsonOptions = new()
14+
{
15+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
16+
};
17+
18+
public async Task Invoke(HttpContext context)
19+
{
20+
try
21+
{
22+
await next(context);
23+
}
24+
catch (RouteBindingException ex)
25+
{
26+
logger.LogWarning(ex, "Route binding error: {Message}", ex.Message);
27+
28+
var problemDetails = _problemDetailsFactory.Create(context, ex);
29+
30+
context.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
31+
context.Response.ContentType = "application/problem+json";
32+
33+
var json = JsonSerializer.Serialize(problemDetails, JsonOptions);
34+
await context.Response.WriteAsync(json);
35+
}
36+
}
37+
38+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.DependencyInjection.Extensions;
3+
4+
namespace EntityInjector.Route.Exceptions.Middleware;
5+
6+
public static class RouteBindingServiceCollectionExtensions
7+
{
8+
public static IServiceCollection AddRouteBinding(this IServiceCollection services)
9+
{
10+
// Register default formatter if user hasn't already
11+
services.TryAddSingleton<IRouteBindingProblemDetailsFactory, DefaultRouteBindingProblemDetailsFactory>();
12+
return services;
13+
}
14+
}
Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,84 @@
1+
using Microsoft.AspNetCore.Http;
2+
13
namespace EntityInjector.Route.Exceptions;
24

3-
public class InternalServerErrorException(string message) : Exception(message);
5+
public abstract class RouteBindingException(string message, Exception? inner = null)
6+
: Exception(message, inner)
7+
{
8+
public abstract int StatusCode { get; }
9+
}
10+
11+
public sealed class RouteEntityNotFoundException(string entityName, object? id)
12+
: RouteBindingException($"No {entityName} found for ID '{id}'.")
13+
{
14+
public override int StatusCode => StatusCodes.Status404NotFound;
15+
16+
public string EntityName { get; } = entityName;
17+
public object? Id { get; } = id;
18+
}
19+
20+
public sealed class MissingRouteAttributeException(string parameterName, string expectedAttribute)
21+
: RouteBindingException($"Missing required {expectedAttribute} on action parameter '{parameterName}'.")
22+
{
23+
public override int StatusCode => StatusCodes.Status400BadRequest;
24+
}
25+
26+
public sealed class UnsupportedBindingTypeException(Type targetType)
27+
: RouteBindingException($"The type '{targetType.Name}' is not supported for route binding.")
28+
{
29+
public override int StatusCode => StatusCodes.Status400BadRequest;
30+
31+
public Type TargetType { get; } = targetType;
32+
}
33+
34+
public sealed class BindingReceiverNotRegisteredException(Type receiverType)
35+
: RouteBindingException($"No binding receiver registered for type '{receiverType.FullName}'.")
36+
{
37+
public override int StatusCode => StatusCodes.Status500InternalServerError;
38+
39+
public Type ReceiverType { get; } = receiverType;
40+
}
41+
42+
public sealed class BindingReceiverContractException(string methodName, Type receiverType)
43+
: RouteBindingException($"Expected method '{methodName}' not found on receiver type '{receiverType.Name}'.")
44+
{
45+
public override int StatusCode => StatusCodes.Status500InternalServerError;
46+
47+
public string MethodName { get; } = methodName;
48+
public Type ReceiverType { get; } = receiverType;
49+
}
50+
51+
public sealed class UnexpectedBindingResultException(Type expected, Type? actual)
52+
: RouteBindingException($"Expected result of type '{expected.Name}', but got '{actual?.Name ?? "null"}'.")
53+
{
54+
public override int StatusCode => StatusCodes.Status500InternalServerError;
55+
56+
public Type ExpectedType { get; } = expected;
57+
public Type? ActualType { get; } = actual;
58+
}
59+
60+
public sealed class MissingRouteParameterException(string parameterName)
61+
: RouteBindingException($"Route parameter '{parameterName}' was not found. Ensure it is correctly specified in the route.")
62+
{
63+
public override int StatusCode => StatusCodes.Status400BadRequest;
64+
65+
public string ParameterName { get; } = parameterName;
66+
}
67+
68+
public sealed class InvalidRouteParameterFormatException(string parameterName, Type expectedType, Type actualType)
69+
: RouteBindingException($"Route parameter '{parameterName}' is of type '{actualType.Name}', but type '{expectedType.Name}' was expected.")
70+
{
71+
public override int StatusCode => StatusCodes.Status422UnprocessableEntity;
72+
73+
public string ParameterName { get; } = parameterName;
74+
public Type ExpectedType { get; } = expectedType;
75+
public Type ActualType { get; } = actualType;
76+
}
77+
78+
public sealed class EmptyRouteSegmentListException(string parameterName)
79+
: RouteBindingException($"Route parameter '{parameterName}' did not contain any valid string segments.")
80+
{
81+
public override int StatusCode => StatusCodes.Status422UnprocessableEntity;
482

5-
public class NotFoundException(string message) : Exception(message);
83+
public string ParameterName { get; } = parameterName;
84+
}

EntityInjector.Route/Middleware/BindingMetadata/Collection/GuidCollectionBindingMetadataProvicer.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,30 @@ protected override List<Guid> GetIds(ActionContext context, string argumentName)
1111
var routeValue = context.HttpContext.GetRouteValue(argumentName);
1212

1313
if (routeValue == null)
14-
throw new InternalServerErrorException(
15-
$"No route value found for parameter '{argumentName}'. Ensure it's included in the route.");
14+
throw new MissingRouteParameterException(argumentName);
1615

1716
var rawString = routeValue.ToString();
1817
if (string.IsNullOrWhiteSpace(rawString))
19-
throw new InternalServerErrorException(
20-
$"Route parameter '{argumentName}' is present but empty. Expected a comma-separated list of GUIDs.");
18+
throw new InvalidRouteParameterFormatException(argumentName, typeof(List<Guid>), typeof(string));
2119

2220
var segments = rawString.Split(',');
23-
2421
var invalidSegments = new List<string>();
2522
var parsedGuids = new List<Guid>();
2623

2724
foreach (var segment in segments)
25+
{
2826
if (Guid.TryParse(segment, out var parsed))
2927
parsedGuids.Add(parsed);
3028
else
3129
invalidSegments.Add(segment);
30+
}
3231

3332
if (invalidSegments.Any())
34-
throw new InternalServerErrorException(
35-
$"The following values in route parameter '{argumentName}' are not valid GUIDs: {string.Join(", ", invalidSegments)}.");
33+
throw new InvalidRouteParameterFormatException(
34+
argumentName,
35+
typeof(Guid),
36+
typeof(string)
37+
);
3638

3739
return parsedGuids;
3840
}

EntityInjector.Route/Middleware/BindingMetadata/Collection/IntCollectionBindingMetadataProvicer.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,30 @@ protected override List<int> GetIds(ActionContext context, string argumentName)
1111
var routeValue = context.HttpContext.GetRouteValue(argumentName);
1212

1313
if (routeValue == null)
14-
throw new InternalServerErrorException(
15-
$"No route value found for parameter '{argumentName}'. Ensure it's included in the route.");
14+
throw new MissingRouteParameterException(argumentName);
1615

1716
var rawString = routeValue.ToString();
1817
if (string.IsNullOrWhiteSpace(rawString))
19-
throw new InternalServerErrorException(
20-
$"Route parameter '{argumentName}' is present but empty. Expected a comma-separated list of ints.");
18+
throw new InvalidRouteParameterFormatException(argumentName, typeof(List<int>), typeof(string));
2119

2220
var segments = rawString.Split(',');
23-
2421
var invalidSegments = new List<string>();
2522
var parsedInts = new List<int>();
2623

2724
foreach (var segment in segments)
25+
{
2826
if (int.TryParse(segment, out var parsed))
2927
parsedInts.Add(parsed);
3028
else
3129
invalidSegments.Add(segment);
30+
}
3231

3332
if (invalidSegments.Any())
34-
throw new InternalServerErrorException(
35-
$"The following values in route parameter '{argumentName}' are not valid ints: {string.Join(", ", invalidSegments)}.");
33+
throw new InvalidRouteParameterFormatException(
34+
argumentName,
35+
typeof(int),
36+
typeof(string)
37+
);
3638

3739
return parsedInts;
3840
}

EntityInjector.Route/Middleware/BindingMetadata/Collection/StringCollectionBindingMetadataProvicer.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,18 @@
44

55
namespace EntityInjector.Route.Middleware.BindingMetadata.Collection;
66

7-
public class
8-
StringCollectionBindingMetadataProvider<TValue> : FromRouteToCollectionBindingMetadataProvider<string, TValue>
7+
public class StringCollectionBindingMetadataProvider<TValue> : FromRouteToCollectionBindingMetadataProvider<string, TValue>
98
{
109
protected override List<string> GetIds(ActionContext context, string argumentName)
1110
{
1211
var routeValue = context.HttpContext.GetRouteValue(argumentName);
1312

1413
if (routeValue == null)
15-
throw new InternalServerErrorException(
16-
$"No route value found for parameter '{argumentName}'. Ensure it's included in the route.");
14+
throw new MissingRouteParameterException(argumentName);
1715

1816
var rawString = routeValue.ToString();
1917
if (string.IsNullOrWhiteSpace(rawString))
20-
throw new InternalServerErrorException(
21-
$"Route parameter '{argumentName}' is present but empty. Expected a comma-separated list of GUIDs.");
18+
throw new InvalidRouteParameterFormatException(argumentName, typeof(string), routeValue.GetType());
2219

2320
var segments = rawString
2421
.Split(',', StringSplitOptions.RemoveEmptyEntries)
@@ -27,8 +24,7 @@ protected override List<string> GetIds(ActionContext context, string argumentNam
2724
.ToList();
2825

2926
if (segments.Count == 0)
30-
throw new InternalServerErrorException(
31-
$"Route parameter '{argumentName}' did not contain any valid string segments.");
27+
throw new EmptyRouteSegmentListException(argumentName);
3228

3329
return segments;
3430
}

0 commit comments

Comments
 (0)