Troubleshooting

Common issues and solutions when using Response Formatting.


Responses Not Being Wrapped

Problem: API responses are returned without the envelope wrapper.

Possible causes:

  1. Response formatting disabled:
// Check this is enabled
options.Response.IsEnabled = true;  // default
  1. Minimal API middleware not registered:
var app = builder.Build();

var api = app.UseAspNetConventions();

api.MapGet("/api/test", () => Results.Ok("test"));

app.Run();
  1. Hook returning false:
// Check if a hook is skipping your responses
options.Response.Hooks.ShouldWrapResponseAsync = async (result, request) =>
{
    Console.WriteLine($"Checking: {request.Path}");  // Debug
    return true;
};
  1. Non-JSON result types:
    Response formatting only wraps ObjectResult types. File downloads, redirects, and other non-JSON results pass through unchanged.

Double-Wrapped Responses

Problem: Responses appear wrapped twice with nested envelopes.

Cause: Your code is returning a pre-wrapped response, and AspNetConventions wraps it again.

Solution: Implement IsWrappedResponse in your custom builder:

public class MyResponseBuilder : IResponseBuilder
{
    public bool IsWrappedResponse(object? value)
    {
        // Return true for your wrapper type to prevent double-wrapping
        return value is MyApiResponse;
    }

    public object BuildResponse(ApiResult apiResult, RequestDescriptor request)
    {
        // ...
    }
}

Or avoid returning pre-wrapped responses from your controllers:

// Don't do this - will be double-wrapped
return Ok(new { success = true, data = user });

// Do this instead - let AspNetConventions wrap it
return ApiResults.Ok(user);

Minimal API Endpoints Not Wrapped

Problem: MVC Controller responses are wrapped, but Minimal API responses are not.

Cause: UseAspNetConventions() was not called, or endpoints were mapped on app directly instead of on the returned group.

Solution: Call UseAspNetConventions() after Build() and map endpoints on the returned RouteGroupBuilder:

var app = builder.Build();

var api = app.UseAspNetConventions();

api.MapGet("/api/test", () => Results.Ok("test"));

app.Run();

Metadata TraceId Always Null

Problem: The metadata.traceId field is always null in responses.

Cause: Distributed tracing is not configured, or Activity.Current is null.

Solution: Ensure tracing middleware is configured:

var builder = WebApplication.CreateBuilder(args);

// Add OpenTelemetry or similar tracing
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation());

var app = builder.Build();

// Or ensure routing is added
app.UseRouting();
var api = app.UseAspNetConventions();

If you don’t need traceId, disable metadata:

options.Response.IncludeMetadata = false;

Problem: Pagination links have wrong parameter names or base URL.

Solution: Configure pagination options:

options.Response.Pagination.PageNumberParameterName = "page";  // default
options.Response.Pagination.PageSizeParameterName = "pageSize";  // default

Make sure you’re passing the correct parameters to Paginate():

// Correct - pass actual page and pageSize
return ApiResults.Paginate(items, totalCount, page, pageSize);

// Wrong - missing page parameters
return ApiResults.Paginate(items, totalCount);  // Won't generate links correctly

Validation Errors Not Formatted

Problem: Model validation errors return raw ValidationProblemDetails instead of the wrapped format.

Cause: The default ASP.NET Core validation filter runs before AspNetConventions.

Solution: Use ApiResults.BadRequest(ModelState) for validation errors:

[HttpPost]
public ActionResult Create([FromBody] CreateRequest request)
{
    if (!ModelState.IsValid)
        return ApiResults.BadRequest(ModelState);  // Use this

    // Don't use: return ApiResults.Ok(ModelState); / Ok(ModelState);

    return ApiResults.Created(result);
}

File/Redirect Results Being Wrapped

Problem: File downloads or redirects are being processed by response formatting.

Answer: This shouldn’t happen. AspNetConventions only wraps ObjectResult types. If you’re seeing issues:

  1. Verify the result type:
// These should NOT be wrapped:
return File(bytes, "application/pdf");
return Redirect("/other-page");
return PhysicalFile("/path/to/file", "image/png");

// These WILL be wrapped:
return Ok(new { data = "test" });
return new ObjectResult(data);
  1. Check for custom middleware that might be converting results.

Different Envelope Per Controller

Problem: You want different response formats for different controllers.

Solution: Response builders are global, but you can implement conditional logic:

public class ConditionalResponseBuilder : IResponseBuilder
{
    public bool IsWrappedResponse(object? value) => false;

    public object BuildResponse(ApiResult apiResult, RequestDescriptor request)
    {
        // Different format for v2 API
        if (request.Path.StartsWith("/api/v2"))
        {
            return new
            {
                result = apiResult.GetValue(),
                error = apiResult.IsSuccess ? null : apiResult.Message
            };
        }

        // Default format
        return new
        {
            status = apiResult.IsSuccess ? "success" : "error",
            data = apiResult.GetValue(),
            message = apiResult.Message
        };
    }
}

Exception Details Showing in Production

Problem: Stack traces and exception details are visible in production responses.

Solution: Check the IncludeExceptionDetails setting:

options.Response.ErrorResponse.IncludeExceptionDetails = false;  // Explicit

// Or use auto-detection (recommended)
options.Response.ErrorResponse.IncludeExceptionDetails = null;  // default
// This includes details only in Development environment

Verify your environment:

if (builder.Environment.IsDevelopment())
{
    options.Response.ErrorResponse.IncludeExceptionDetails = true;
}
else
{
    options.Response.ErrorResponse.IncludeExceptionDetails = false;
}

ApiResults Methods Not Found

Problem: Compiler error: 'ApiResults' does not contain a definition for 'Ok'.

Solution: Add the correct using statement:

using AspNetConventions.Http;  // Add this

public class MyController : ControllerBase
{
    public ActionResult Get()
    {
        return ApiResults.Ok("test");  // Now works
    }
}

Custom Builder Not Being Used

Problem: You registered a custom builder but responses still use the default format.

Solution: Verify registration:

builder.Services.AddControllers()
    .AddAspNetConventions(options =>
    {
        // Make sure this is set
        options.Response.ResponseBuilder = new MyResponseBuilder();
        options.Response.ErrorResponseBuilder = new MyErrorResponseBuilder();
    });

If using DI:

// Register the builder
builder.Services.AddSingleton<IResponseBuilder, MyResponseBuilder>();

// Then resolve and assign
var serviceProvider = builder.Services.BuildServiceProvider();
options.Response.ResponseBuilder = serviceProvider.GetRequiredService<IResponseBuilder>();

Debugging Response Formatting

Use the AfterResponseWrapAsync hook to inspect responses:

options.Response.Hooks.AfterResponseWrapAsync = async (wrapped, result, request) =>
{
    Console.WriteLine($"=== Response Debug ===");
    Console.WriteLine($"Path: {request.Path}");
    Console.WriteLine($"Status: {result.StatusCode}");
    Console.WriteLine($"Type: {result.Type}");
    Console.WriteLine($"IsSuccess: {result.IsSuccess}");
    Console.WriteLine($"Value Type: {result.GetValue()?.GetType().Name ?? "null"}");
    Console.WriteLine($"Wrapped Type: {wrapped?.GetType().Name ?? "null"}");
};

This helps identify:

  • Which responses are being processed
  • What data is available in the ApiResult
  • Whether wrapping is occurring correctly