Skip to content
Draft
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
17 changes: 15 additions & 2 deletions src/Http/Http.Results/src/BadRequestOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -42,7 +43,7 @@ internal BadRequest(TValue? error)
int? IStatusCodeHttpResult.StatusCode => StatusCode;

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
public async Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);

Expand All @@ -53,7 +54,19 @@ public Task ExecuteAsync(HttpContext httpContext)
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return HttpResultsHelper.WriteResultAsJsonAsync(
// If the value is a ProblemDetails, attempt to use IProblemDetailsService for writing
// This enables customizations via ProblemDetailsOptions.CustomizeProblemDetails
if (Value is ProblemDetails problemDetails)
{
var problemDetailsService = httpContext.RequestServices.GetService<IProblemDetailsService>();
if (problemDetailsService is not null &&
await problemDetailsService.TryWriteAsync(new() { HttpContext = httpContext, ProblemDetails = problemDetails }))
{
return;
}
}

await HttpResultsHelper.WriteResultAsJsonAsync(
httpContext,
logger: logger,
Value);
Expand Down
17 changes: 15 additions & 2 deletions src/Http/Http.Results/src/ConflictOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -42,7 +43,7 @@ internal Conflict(TValue? error)
int? IStatusCodeHttpResult.StatusCode => StatusCode;

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
public async Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);

Expand All @@ -53,7 +54,19 @@ public Task ExecuteAsync(HttpContext httpContext)
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return HttpResultsHelper.WriteResultAsJsonAsync(
// If the value is a ProblemDetails, attempt to use IProblemDetailsService for writing
// This enables customizations via ProblemDetailsOptions.CustomizeProblemDetails
if (Value is ProblemDetails problemDetails)
{
var problemDetailsService = httpContext.RequestServices.GetService<IProblemDetailsService>();
if (problemDetailsService is not null &&
await problemDetailsService.TryWriteAsync(new() { HttpContext = httpContext, ProblemDetails = problemDetails }))
{
return;
}
}

await HttpResultsHelper.WriteResultAsJsonAsync(
httpContext,
logger: logger,
Value);
Expand Down
17 changes: 15 additions & 2 deletions src/Http/Http.Results/src/InternalServerErrorOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -42,7 +43,7 @@ internal InternalServerError(TValue? error)
int? IStatusCodeHttpResult.StatusCode => StatusCode;

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
public async Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);

Expand All @@ -53,7 +54,19 @@ public Task ExecuteAsync(HttpContext httpContext)
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return HttpResultsHelper.WriteResultAsJsonAsync(
// If the value is a ProblemDetails, attempt to use IProblemDetailsService for writing
// This enables customizations via ProblemDetailsOptions.CustomizeProblemDetails
if (Value is ProblemDetails problemDetails)
{
var problemDetailsService = httpContext.RequestServices.GetService<IProblemDetailsService>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The typical pattern we use here is something like:

if (problemDetailsService is null || !await problemDetailsService.TryWriteAsync(new() { HttpContext = httpContext, ProblemDetails = ProblemDetails }))
        {
            await HttpResultsHelper.WriteResultAsJsonAsync(
                httpContext,
                logger,
                value: ProblemDetails,
                ContentType);
        }

See https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Results/src/ProblemHttpResult.cs for an example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot Please adopt the patterm that @captainsafia shows here.

if (problemDetailsService is not null &&
await problemDetailsService.TryWriteAsync(new() { HttpContext = httpContext, ProblemDetails = problemDetails }))
{
return;
}
}

await HttpResultsHelper.WriteResultAsJsonAsync(
httpContext,
logger: logger,
Value);
Expand Down
17 changes: 15 additions & 2 deletions src/Http/Http.Results/src/NotFoundOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -41,7 +42,7 @@ internal NotFound(TValue? value)
int? IStatusCodeHttpResult.StatusCode => StatusCode;

/// <inheritdoc/>
public Task ExecuteAsync(HttpContext httpContext)
public async Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);

Expand All @@ -52,7 +53,19 @@ public Task ExecuteAsync(HttpContext httpContext)
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return HttpResultsHelper.WriteResultAsJsonAsync(
// If the value is a ProblemDetails, attempt to use IProblemDetailsService for writing
// This enables customizations via ProblemDetailsOptions.CustomizeProblemDetails
if (Value is ProblemDetails problemDetails)
{
var problemDetailsService = httpContext.RequestServices.GetService<IProblemDetailsService>();
if (problemDetailsService is not null &&
await problemDetailsService.TryWriteAsync(new() { HttpContext = httpContext, ProblemDetails = problemDetails }))
{
return;
}
}

await HttpResultsHelper.WriteResultAsJsonAsync(
httpContext,
logger: logger,
Value);
Expand Down
17 changes: 15 additions & 2 deletions src/Http/Http.Results/src/UnprocessableEntityOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -42,7 +43,7 @@ internal UnprocessableEntity(TValue? value)
int? IStatusCodeHttpResult.StatusCode => StatusCode;

/// <inheritdoc />
public Task ExecuteAsync(HttpContext httpContext)
public async Task ExecuteAsync(HttpContext httpContext)
{
ArgumentNullException.ThrowIfNull(httpContext);

Expand All @@ -53,7 +54,19 @@ public Task ExecuteAsync(HttpContext httpContext)
HttpResultsHelper.Log.WritingResultAsStatusCode(logger, StatusCode);
httpContext.Response.StatusCode = StatusCode;

return HttpResultsHelper.WriteResultAsJsonAsync(
// If the value is a ProblemDetails, attempt to use IProblemDetailsService for writing
// This enables customizations via ProblemDetailsOptions.CustomizeProblemDetails
if (Value is ProblemDetails problemDetails)
{
var problemDetailsService = httpContext.RequestServices.GetService<IProblemDetailsService>();
if (problemDetailsService is not null &&
await problemDetailsService.TryWriteAsync(new() { HttpContext = httpContext, ProblemDetails = problemDetails }))
{
return;
}
}

await HttpResultsHelper.WriteResultAsJsonAsync(
httpContext,
logger: logger,
Value);
Expand Down
157 changes: 157 additions & 0 deletions src/Http/Http.Results/test/BadRequestOfTResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,163 @@ public void BadRequestObjectResult_Implements_IValueHttpResultOfT_Correctly()
Assert.Equal(value, result.Value);
}

[Fact]
public async Task BadRequestObjectResult_WithProblemDetails_UsesDefaultsFromProblemDetailsService()
{
// Arrange
var details = new ProblemDetails();
var result = new BadRequest<ProblemDetails>(details);
var stream = new MemoryStream();
var services = CreateServiceCollection()
.AddProblemDetails(options => options.CustomizeProblemDetails = context => context.ProblemDetails.Extensions["customProperty"] = "customValue")
.BuildServiceProvider();
var httpContext = new DefaultHttpContext()
{
RequestServices = services,
Response =
{
Body = stream,
},
};

// Act
await result.ExecuteAsync(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = System.Text.Json.JsonSerializer.Deserialize<ProblemDetails>(stream, new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));
Assert.NotNull(responseDetails);
Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status);
Assert.True(responseDetails.Extensions.ContainsKey("customProperty"));
Assert.Equal("customValue", responseDetails.Extensions["customProperty"]?.ToString());
}

[Fact]
public async Task BadRequestObjectResult_WithProblemDetails_AppliesTraceIdFromService()
{
// Arrange
var details = new ProblemDetails();
var result = new BadRequest<ProblemDetails>(details);
var stream = new MemoryStream();
var services = CreateServiceCollection()
.AddProblemDetails()
.BuildServiceProvider();
var httpContext = new DefaultHttpContext()
{
RequestServices = services,
Response =
{
Body = stream,
},
};

// Act
await result.ExecuteAsync(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = System.Text.Json.JsonSerializer.Deserialize<ProblemDetails>(stream, new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));
Assert.NotNull(responseDetails);
Assert.True(responseDetails.Extensions.ContainsKey("traceId"));
Assert.NotNull(responseDetails.Extensions["traceId"]);
}

[Fact]
public async Task BadRequestObjectResult_WithProblemDetails_FallsBackWhenServiceNotRegistered()
{
// Arrange
var details = new ProblemDetails { Title = "Test Error" };
var result = new BadRequest<ProblemDetails>(details);
var stream = new MemoryStream();
var httpContext = new DefaultHttpContext()
{
RequestServices = CreateServices(),
Response =
{
Body = stream,
},
};

// Act
await result.ExecuteAsync(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = System.Text.Json.JsonSerializer.Deserialize<ProblemDetails>(stream, new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));
Assert.NotNull(responseDetails);
Assert.Equal("Test Error", responseDetails.Title);
Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status);
}

[Fact]
public async Task BadRequestObjectResult_WithHttpValidationProblemDetails_UsesDefaultsFromProblemDetailsService()
{
// Arrange
var details = new HttpValidationProblemDetails();
var result = new BadRequest<HttpValidationProblemDetails>(details);
var stream = new MemoryStream();
var services = CreateServiceCollection()
.AddProblemDetails(options => options.CustomizeProblemDetails = context => context.ProblemDetails.Extensions["customValidation"] = "applied")
.BuildServiceProvider();
var httpContext = new DefaultHttpContext()
{
RequestServices = services,
Response =
{
Body = stream,
},
};

// Act
await result.ExecuteAsync(httpContext);

// Assert
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
stream.Position = 0;
var responseDetails = System.Text.Json.JsonSerializer.Deserialize<HttpValidationProblemDetails>(stream, new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web));
Assert.NotNull(responseDetails);
Assert.True(responseDetails.Extensions.ContainsKey("customValidation"));
Assert.Equal("applied", responseDetails.Extensions["customValidation"]?.ToString());
}

[Fact]
public async Task BadRequestObjectResult_WithNonProblemDetails_DoesNotUseProblemDetailsService()
{
// Arrange
var details = new { error = "test error" };
var result = new BadRequest<object>(details);
var stream = new MemoryStream();
var customizationCalled = false;
var services = CreateServiceCollection()
.AddProblemDetails(options => options.CustomizeProblemDetails = context => customizationCalled = true)
.BuildServiceProvider();
var httpContext = new DefaultHttpContext()
{
RequestServices = services,
Response =
{
Body = stream,
},
};

// Act
await result.ExecuteAsync(httpContext);

// Assert
Assert.False(customizationCalled, "CustomizeProblemDetails should not be called for non-ProblemDetails types");
Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode);
}

private static ServiceCollection CreateServiceCollection()
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
return services;
}

Comment on lines +176 to +332
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

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

Comprehensive test coverage has been added for BadRequest, but similar tests are missing for the other result types that received the same implementation changes (Conflict, NotFound, InternalServerError, and UnprocessableEntity). These test cases should be duplicated for those result types to ensure consistent behavior and catch any regressions.

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

Choose a reason for hiding this comment

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

@copilot Please add tests for the other result types.

private static void PopulateMetadata<TResult>(MethodInfo method, EndpointBuilder builder)
where TResult : IEndpointMetadataProvider => TResult.PopulateMetadata(method, builder);

Expand Down
Loading