Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions EntityInjector.Route/EntityInjector.Route.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>EntityInjector.Route</PackageId>
<Version>1.0.0</Version>
<Version>1.0.1</Version>
<Company>Devies</Company>
<Authors>John Johansson; Erik Jergéus</Authors>
<TargetFramework>netstandard2.1</TargetFramework>
Expand All @@ -18,8 +18,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.3.0" />
<PackageReference Include="System.Text.Json" Version="9.0.7" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace EntityInjector.Route.Exceptions.Middleware;

public class DefaultRouteBindingProblemDetailsFactory : IRouteBindingProblemDetailsFactory
{
public ProblemDetails Create(HttpContext context, RouteBindingException exception)
{
return new ProblemDetails
{
Status = exception.StatusCode,
Detail = exception.Message,
Instance = context.Request.Path
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace EntityInjector.Route.Exceptions.Middleware;

public interface IRouteBindingProblemDetailsFactory
{
ProblemDetails Create(HttpContext context, RouteBindingException exception);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Builder;

namespace EntityInjector.Route.Exceptions.Middleware;

public static class RouteBindingApplicationBuilderExtensions
{
public static IApplicationBuilder UseRouteBinding(this IApplicationBuilder app)
{
return app.UseMiddleware<RouteBindingExceptionMiddleware>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace EntityInjector.Route.Exceptions.Middleware;

public class RouteBindingExceptionMiddleware(
RequestDelegate next,
ILogger<RouteBindingExceptionMiddleware> logger,
IRouteBindingProblemDetailsFactory? problemDetailsFactory = null)
{
private readonly IRouteBindingProblemDetailsFactory _problemDetailsFactory = problemDetailsFactory ?? new DefaultRouteBindingProblemDetailsFactory();
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (RouteBindingException ex)
{
logger.LogWarning(ex, "Route binding error: {Message}", ex.Message);

var problemDetails = _problemDetailsFactory.Create(context, ex);

context.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/problem+json";

var json = JsonSerializer.Serialize(problemDetails, JsonOptions);
await context.Response.WriteAsync(json);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace EntityInjector.Route.Exceptions.Middleware;

public static class RouteBindingServiceCollectionExtensions
{
public static IServiceCollection AddRouteBinding(this IServiceCollection services)
{
// Register default formatter if user hasn't already
services.TryAddSingleton<IRouteBindingProblemDetailsFactory, DefaultRouteBindingProblemDetailsFactory>();
return services;
}
}
83 changes: 81 additions & 2 deletions EntityInjector.Route/Exceptions/StatusExceptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,84 @@
using Microsoft.AspNetCore.Http;

namespace EntityInjector.Route.Exceptions;

public class InternalServerErrorException(string message) : Exception(message);
public abstract class RouteBindingException(string message, Exception? inner = null)
: Exception(message, inner)
{
public abstract int StatusCode { get; }
}

public sealed class RouteEntityNotFoundException(string entityName, object? id)
: RouteBindingException($"No {entityName} found for ID '{id}'.")
{
public override int StatusCode => StatusCodes.Status404NotFound;

public string EntityName { get; } = entityName;
public object? Id { get; } = id;
}

public sealed class MissingRouteAttributeException(string parameterName, string expectedAttribute)
: RouteBindingException($"Missing required {expectedAttribute} on action parameter '{parameterName}'.")
{
public override int StatusCode => StatusCodes.Status400BadRequest;
}

public sealed class UnsupportedBindingTypeException(Type targetType)
: RouteBindingException($"The type '{targetType.Name}' is not supported for route binding.")
{
public override int StatusCode => StatusCodes.Status400BadRequest;

public Type TargetType { get; } = targetType;
}

public sealed class BindingReceiverNotRegisteredException(Type receiverType)
: RouteBindingException($"No binding receiver registered for type '{receiverType.FullName}'.")
{
public override int StatusCode => StatusCodes.Status500InternalServerError;

public Type ReceiverType { get; } = receiverType;
}

public sealed class BindingReceiverContractException(string methodName, Type receiverType)
: RouteBindingException($"Expected method '{methodName}' not found on receiver type '{receiverType.Name}'.")
{
public override int StatusCode => StatusCodes.Status500InternalServerError;

public string MethodName { get; } = methodName;
public Type ReceiverType { get; } = receiverType;
}

public sealed class UnexpectedBindingResultException(Type expected, Type? actual)
: RouteBindingException($"Expected result of type '{expected.Name}', but got '{actual?.Name ?? "null"}'.")
{
public override int StatusCode => StatusCodes.Status500InternalServerError;

public Type ExpectedType { get; } = expected;
public Type? ActualType { get; } = actual;
}

public sealed class MissingRouteParameterException(string parameterName)
: RouteBindingException($"Route parameter '{parameterName}' was not found. Ensure it is correctly specified in the route.")
{
public override int StatusCode => StatusCodes.Status400BadRequest;

public string ParameterName { get; } = parameterName;
}

public sealed class InvalidRouteParameterFormatException(string parameterName, Type expectedType, Type actualType)
: RouteBindingException($"Route parameter '{parameterName}' is of type '{actualType.Name}', but type '{expectedType.Name}' was expected.")
{
public override int StatusCode => StatusCodes.Status422UnprocessableEntity;

public string ParameterName { get; } = parameterName;
public Type ExpectedType { get; } = expectedType;
public Type ActualType { get; } = actualType;
}

public sealed class EmptyRouteSegmentListException(string parameterName)
: RouteBindingException($"Route parameter '{parameterName}' did not contain any valid string segments.")
{
public override int StatusCode => StatusCodes.Status422UnprocessableEntity;

public class NotFoundException(string message) : Exception(message);
public string ParameterName { get; } = parameterName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,30 @@ protected override List<Guid> GetIds(ActionContext context, string argumentName)
var routeValue = context.HttpContext.GetRouteValue(argumentName);

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

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

var segments = rawString.Split(',');

var invalidSegments = new List<string>();
var parsedGuids = new List<Guid>();

foreach (var segment in segments)
{
if (Guid.TryParse(segment, out var parsed))
parsedGuids.Add(parsed);
else
invalidSegments.Add(segment);
}

if (invalidSegments.Any())
throw new InternalServerErrorException(
$"The following values in route parameter '{argumentName}' are not valid GUIDs: {string.Join(", ", invalidSegments)}.");
throw new InvalidRouteParameterFormatException(
argumentName,
typeof(Guid),
typeof(string)
);

return parsedGuids;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,30 @@ protected override List<int> GetIds(ActionContext context, string argumentName)
var routeValue = context.HttpContext.GetRouteValue(argumentName);

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

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

var segments = rawString.Split(',');

var invalidSegments = new List<string>();
var parsedInts = new List<int>();

foreach (var segment in segments)
{
if (int.TryParse(segment, out var parsed))
parsedInts.Add(parsed);
else
invalidSegments.Add(segment);
}

if (invalidSegments.Any())
throw new InternalServerErrorException(
$"The following values in route parameter '{argumentName}' are not valid ints: {string.Join(", ", invalidSegments)}.");
throw new InvalidRouteParameterFormatException(
argumentName,
typeof(int),
typeof(string)
);

return parsedInts;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,18 @@

namespace EntityInjector.Route.Middleware.BindingMetadata.Collection;

public class
StringCollectionBindingMetadataProvider<TValue> : FromRouteToCollectionBindingMetadataProvider<string, TValue>
public class StringCollectionBindingMetadataProvider<TValue> : FromRouteToCollectionBindingMetadataProvider<string, TValue>
{
protected override List<string> GetIds(ActionContext context, string argumentName)
{
var routeValue = context.HttpContext.GetRouteValue(argumentName);

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

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

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

if (segments.Count == 0)
throw new InternalServerErrorException(
$"Route parameter '{argumentName}' did not contain any valid string segments.");
throw new EmptyRouteSegmentListException(argumentName);

return segments;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,13 @@ protected override Guid GetId(ActionContext context, string argumentName)
var routeValue = context.HttpContext.GetRouteValue(argumentName);

if (routeValue == null)
throw new InternalServerErrorException(
$"Route value for '{argumentName}' was not found. Make sure it is part of the route pattern.");
throw new MissingRouteParameterException(argumentName);

try
return routeValue switch
{
return routeValue switch
{
Guid g => g,
string s when Guid.TryParse(s, out var parsed) => parsed,
_ => throw new InvalidCastException(
$"Route value '{argumentName}' is neither a GUID nor a string that can be parsed into a GUID.")
};
}
catch (Exception)
{
throw new InternalServerErrorException(
$"Failed to parse route value '{argumentName}' as a GUID. Value: '{routeValue}'");
}
Guid g => g,
string s when Guid.TryParse(s, out var parsed) => parsed,
_ => throw new InvalidRouteParameterFormatException(argumentName, typeof(Guid), routeValue.GetType())
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ protected override int GetId(ActionContext context, string argumentName)
var routeValue = context.HttpContext.GetRouteValue(argumentName);

if (routeValue == null)
throw new InternalServerErrorException(
$"Route parameter '{argumentName}' was not found. Ensure it is correctly specified in the route.");
throw new MissingRouteParameterException(argumentName);

return routeValue switch
{
int g => g,
string s => int.Parse(s),
_ => throw new InternalServerErrorException(
$"Route parameter '{argumentName}' must be a non-empty string or an int, but received type '{routeValue.GetType().Name}'.")
_ => throw new InvalidRouteParameterFormatException(argumentName, routeValue.GetType(), routeValue.GetType())
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ protected override string GetId(ActionContext context, string argumentName)
var routeValue = context.HttpContext.GetRouteValue(argumentName);

if (routeValue == null)
throw new InternalServerErrorException(
$"Route parameter '{argumentName}' was not found. Ensure it is correctly specified in the route.");
throw new MissingRouteParameterException(argumentName);

return routeValue switch
{
string s when !string.IsNullOrWhiteSpace(s) => s,
Guid g => g.ToString(),
_ => throw new InternalServerErrorException(
$"Route parameter '{argumentName}' must be a non-empty string or a Guid, but received type '{routeValue.GetType().Name}'.")
_ => throw new InvalidRouteParameterFormatException(argumentName, routeValue.GetType(), routeValue.GetType())
};
}
}
Loading