Skip to content
Open
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
50 changes: 20 additions & 30 deletions src/Http/Http.Extensions/src/RequestDelegateFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -993,41 +993,31 @@ private static Expression CreateParamCheckingResponseWritingMethodCall(Type retu

localVariables[factoryContext.ExtraLocals.Count] = WasParamCheckFailureExpr;

// If filters have been registered, we set the `wasParamCheckFailure` property
// but do not return from the invocation to allow the filters to run.
if (factoryContext.EndpointBuilder.FilterFactories.Count > 0)
{
// if (wasParamCheckFailure)
// {
// httpContext.Response.StatusCode = 400;
// }
// return RequestDelegateFactory.ExecuteObjectReturn(invocationPipeline.Invoke(context) as object);
var checkWasParamCheckFailureWithFilters = Expression.Block(
Expression.IfThen(
WasParamCheckFailureExpr,
Expression.Assign(StatusCodeExpr, Expression.Constant(400))),
AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext)
);

checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailureWithFilters;
// On parameter binding failure:
// - If filters are present (pipeline built -> returnType ValueTask<object?>), run the filter pipeline so that
// filters can observe/modify the 400 response. The handler itself will be skipped by the StatusCode >= 400
// check embedded in the pipeline (see CreateFilterPipeline), allowing filters to customize the error.
// - If no filters are present, just set 400 and short-circuit.
Expression failureBranch;
if (factoryContext.EndpointBuilder.FilterFactories.Count > 0 && returnType == typeof(ValueTask<object?>))
{
failureBranch = Expression.Block(
Expression.Assign(StatusCodeExpr, Expression.Constant(400)),
AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext));
}
else
{
// wasParamCheckFailure ? {
// httpContext.Response.StatusCode = 400;
// return Task.CompletedTask;
// } : {
// return RequestDelegateFactory.ExecuteObjectReturn(invocationPipeline.Invoke(context) as object);
// }
var checkWasParamCheckFailure = Expression.Condition(
WasParamCheckFailureExpr,
Expression.Block(
Expression.Assign(StatusCodeExpr, Expression.Constant(400)),
CompletedTaskExpr),
AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext));
checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure;
failureBranch = Expression.Block(
Expression.Assign(StatusCodeExpr, Expression.Constant(400)),
CompletedTaskExpr);
}

var checkWasParamCheckFailure = Expression.Condition(
WasParamCheckFailureExpr,
failureBranch,
AddResponseWritingToMethodCall(factoryContext.MethodCall!, returnType, factoryContext));
checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure;

return Expression.Block(localVariables, checkParamAndCallMethod);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,164 @@ public void FinallyOnNestedGroups_OuterGroupCanExamineInnerGroup()
Assert.Equal(new[] { "added-from-endpoint-1", "added-from-endpoint-2", "added-from-inner-group", "added-from-outer-group" }, endpoint.Metadata.GetOrderedMetadata<string>());
}

[Fact]
public async Task ParameterBindingFailure_WithEndpointFilterFactory_DoesNotExecuteHandler()
{
// Arrange
var services = new ServiceCollection().AddSingleton(LoggerFactory);
var serviceProvider = services.BuildServiceProvider();

var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
var handlerExecuted = false;

// Act - Add an endpoint filter factory that returns next as-is
builder.MapGet("/test/{id}", (Guid id) =>
{
handlerExecuted = true;
return 1;
}).AddEndpointFilterFactory((_, next) => next);

var dataSource = GetBuilderEndpointDataSource(builder);
var endpoint = Assert.Single(dataSource.Endpoints);

var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
httpContext.Request.RouteValues["id"] = "invalid-guid";

// Act
await endpoint.RequestDelegate!(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
Assert.True(httpContext.Response.Body.Length == 0, "Response body should be empty");
Assert.False(handlerExecuted, "Handler should not have been executed when parameter binding fails");
}

[Fact]
public async Task ParameterBindingFailure_WithoutFilter_DoesNotExecuteHandler()
{
// Arrange
var services = new ServiceCollection().AddSingleton(LoggerFactory);
var serviceProvider = services.BuildServiceProvider();

var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
var handlerExecuted = false;

// Act
builder.MapGet("/test/{id}", (Guid id) =>
{
handlerExecuted = true;
return 1;
});

var dataSource = GetBuilderEndpointDataSource(builder);
var endpoint = Assert.Single(dataSource.Endpoints);

var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
httpContext.Request.RouteValues["id"] = "invalid-guid";

// Act
await endpoint.RequestDelegate!(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
Assert.True(httpContext.Response.Body.Length == 0, "Response body should be empty");
Assert.False(handlerExecuted, "Handler should not have been executed when parameter binding fails");
}

[Fact]
public async Task ParameterBindingFailure_WithFilterFactoryGeneratingResponse_WritesBody()
{
// Arrange
var services = new ServiceCollection().AddSingleton(LoggerFactory);
var serviceProvider = services.BuildServiceProvider();

var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
var handlerExecuted = false;

// Act - Filter explicitly generates custom response for 400 errors
builder.MapGet("/test/{id}", (Guid id) =>
{
handlerExecuted = true;
return 1;
}).AddEndpointFilterFactory((_, next) => async (context) =>
{
if (context.HttpContext.Response.StatusCode == StatusCodes.Status400BadRequest)
{
// Return a simple custom error message
return Results.Json(new { error = "Parameter binding failed" }, statusCode: StatusCodes.Status400BadRequest);
}

return await next(context);
});

var dataSource = GetBuilderEndpointDataSource(builder);
var endpoint = Assert.Single(dataSource.Endpoints);

var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
httpContext.Request.RouteValues["id"] = "invalid-guid";
using var responseBody = new MemoryStream();
httpContext.Response.Body = responseBody;

// Act
await endpoint.RequestDelegate!(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
Assert.False(handlerExecuted, "Handler should not have been executed when parameter binding fails");

// Filter generated JSON error, so body should contain JSON.
responseBody.Seek(0, SeekOrigin.Begin);
using var reader = new StreamReader(responseBody);
var body = await reader.ReadToEndAsync();
Assert.NotEmpty(body);
Assert.Contains("Parameter binding failed", body);
}

[Fact]
public async Task ParameterBindingFailure_WithAddValidation_DoesNotExecuteHandler()
{
// Arrange
var services = new ServiceCollection().AddSingleton(LoggerFactory);
services.AddValidation(); // Register validation filter factory
var serviceProvider = services.BuildServiceProvider();

var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
var handlerExecuted = false;

// Act - Binding failure happens BEFORE validation filter runs
builder.MapGet("/test/{id}", (Guid id) =>
{
handlerExecuted = true;
return 1;
});

var dataSource = GetBuilderEndpointDataSource(builder);
var endpoint = Assert.Single(dataSource.Endpoints);

var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
httpContext.Request.RouteValues["id"] = "invalid-guid";

// Act
await endpoint.RequestDelegate!(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
Assert.True(httpContext.Response.Body.Length == 0, "Response body should be empty");
Assert.False(handlerExecuted, "Handler should not have been executed when parameter binding fails");
}

Comment on lines +1191 to +1226
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AddValidation() extension method requires a reference to Microsoft.Extensions.Validation, but this test project (Microsoft.AspNetCore.Routing.Tests.csproj) doesn't appear to have that reference. This will cause a compilation error.

Either:

  1. Add a reference to Microsoft.Extensions.Validation in the project file, or
  2. Move this test to the Microsoft.AspNetCore.Http.Extensions.Tests project where validation testing is more appropriate, or
  3. Remove this test if validation integration testing is not needed here
Suggested change
[Fact]
public async Task ParameterBindingFailure_WithAddValidation_DoesNotExecuteHandler()
{
// Arrange
var services = new ServiceCollection().AddSingleton(LoggerFactory);
services.AddValidation(); // Register validation filter factory
var serviceProvider = services.BuildServiceProvider();
var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
var handlerExecuted = false;
// Act - Binding failure happens BEFORE validation filter runs
builder.MapGet("/test/{id}", (Guid id) =>
{
handlerExecuted = true;
return 1;
});
var dataSource = GetBuilderEndpointDataSource(builder);
var endpoint = Assert.Single(dataSource.Endpoints);
var httpContext = new DefaultHttpContext
{
RequestServices = serviceProvider
};
httpContext.Request.RouteValues["id"] = "invalid-guid";
// Act
await endpoint.RequestDelegate!(httpContext);
// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
Assert.True(httpContext.Response.Body.Length == 0, "Response body should be empty");
Assert.False(handlerExecuted, "Handler should not have been executed when parameter binding fails");
}
// [Fact]
// public async Task ParameterBindingFailure_WithAddValidation_DoesNotExecuteHandler()
// {
// // Arrange
// var services = new ServiceCollection().AddSingleton(LoggerFactory);
// services.AddValidation(); // Register validation filter factory
// var serviceProvider = services.BuildServiceProvider();
//
// var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
// var handlerExecuted = false;
//
// // Act - Binding failure happens BEFORE validation filter runs
// builder.MapGet("/test/{id}", (Guid id) =>
// {
// handlerExecuted = true;
// return 1;
// });
//
// var dataSource = GetBuilderEndpointDataSource(builder);
// var endpoint = Assert.Single(dataSource.Endpoints);
//
// var httpContext = new DefaultHttpContext
// {
// RequestServices = serviceProvider
// };
// httpContext.Request.RouteValues["id"] = "invalid-guid";
//
// // Act
// await endpoint.RequestDelegate!(httpContext);
//
// // Assert
// Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
// Assert.True(httpContext.Response.Body.Length == 0, "Response body should be empty");
// Assert.False(handlerExecuted, "Handler should not have been executed when parameter binding fails");
// }

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the tests succeeded, so there are no compilation errors at all. In particular, this test focuses on behavior related to parameter-binding failures, so I think it makes sense to keep it here.

class MyService { }

class ServiceAccessingEndpointFilter : IEndpointFilter
Expand Down
Loading