Troubleshooting
Common issues and solutions across all AspNetConventions features.
Route Standardization
Routes Not Being Transformed
Problem: Routes remain in PascalCase despite configuration.
Possible causes:
- Transformation disabled — check that all relevant flags are on:
options.Route.IsEnabled = true;
options.Route.Controllers.IsEnabled = true; // MVC
options.Route.RazorPages.IsEnabled = true; // Razor Pages
options.Route.MinimalApi.IsEnabled = true; // Minimal APIs
- Endpoint is excluded:
// Check these lists for your controller, area, tag, or route pattern
options.Route.Controllers.ExcludeControllers
options.Route.Controllers.ExcludeAreas
options.Route.MinimalApi.ExcludeTags
options.Route.MinimalApi.ExcludeRoutePatterns
- A hook is returning
false— add a debug log to confirm:
options.Route.Hooks.ShouldTransformToken = token =>
{
Console.WriteLine($"Checking token: {token}");
return true;
};
Parameter Binding Fails in Minimal APIs
Problem: After enabling route transformation, Minimal API parameters return null or cause binding errors.
Cause: Minimal APIs bind parameters strictly by name. When {userId} is transformed to {user-id}, the binder can no longer match the value.
Solution A — Explicit binding:
options.Route.MinimalApi.TransformRouteParameters = true;
api.MapGet("/UserAccount/{userId}",
([FromRoute(Name = "user-id")] int userId) => Results.Ok(userId));
Solution B — Leave parameter transformation off (the default):
options.Route.MinimalApi.TransformRouteParameters = false; // default
Query String Parameters Not Binding
Problem: Complex type properties don’t bind from query strings after transformation.
Cause: Query parameter names must match the transformed property names.
public class SearchRequest
{
public string CategoryName { get; set; }
public int PageNumber { get; set; }
}
| URL | |
|---|---|
| Incorrect | /search?CategoryName=Books&PageNumber=1 |
| Correct | /search?category-name=Books&page-number=1 |
Razor Pages Form Fields Not Binding
Problem: Form submissions fail to bind to [BindProperty] properties.
Cause: Form field names must use the transformed names.
<!-- Incorrect -->
<input type="text" name="UserName" />
<!-- Correct -->
<input type="text" name="user-name" />
Using tag helpers handles this automatically:
<input asp-for="UserName" />
<!-- Generates: name="user-name" -->
See Enable Tag Helpers for setup instructions.
Debugging Route Transformations
Use AfterRouteTransform to log all transformations at startup:
options.Route.Hooks.AfterRouteTransform = (newRoute, originalRoute, model) =>
{
Console.WriteLine($"[{model.Identity.Kind}] {originalRoute} → {newRoute}");
};
[MvcAction] api/UserProfile/GetById/{UserId} → api/user-profile/get-by-id/{user-id}
[MinimalApi] /WeatherForecast/{CityName} → /weather-forecast/{city-name}
[RazorPage] UserProfile/Edit/{UserId} → user-profile/edit/{user-id}
Response Formatting
Responses Not Being Wrapped
Problem: API responses are returned without the envelope wrapper.
Possible causes:
- Response formatting disabled:
options.Response.IsEnabled = true; // default
- Minimal API middleware not registered — endpoints must be mapped on the group returned by
UseAspNetConventions():
var api = app.UseAspNetConventions();
api.MapGet("/api/test", () => Results.Ok("test")); // ✓
app.MapGet("/api/test", () => Results.Ok("test")); // ✗ bypasses formatting
- A hook is returning
false:
options.Response.Hooks.ShouldWrapResponseAsync = async (result, request) =>
{
Console.WriteLine($"Checking: {request.Path}");
return true;
};
- Non-JSON result — only
ObjectResulttypes are wrapped. File downloads, redirects, and similar results pass through unchanged.
Double-Wrapped Responses
Problem: Responses appear wrapped twice with nested envelopes.
Cause: Your controller is returning a pre-wrapped object, and AspNetConventions wraps it again.
Solution: Let the library do the wrapping — return raw values:
// Don't: returns a wrapper that gets wrapped again
return Ok(new { success = true, data = user });
// Do: return the value directly
return ApiResults.Ok(user);
If you use a custom IResponseBuilder, implement IsWrappedResponse to signal your own type:
public bool IsWrappedResponse(object? value) => value is MyApiResponse;
Validation Errors Not Formatted
Problem: Model validation errors return raw ValidationProblemDetails instead of the wrapped format.
Solution: Use ApiResults.BadRequest(ModelState) or BadRequest(ModelState):
if (!ModelState.IsValid)
return ApiResults.BadRequest(ModelState);
Pagination Links Incorrect
Problem: Pagination links have wrong parameter names or base URL.
Solution: Verify your pagination parameter name configuration and pass the correct values to Paginate():
options.Response.Pagination.PageNumberParameterName = "page-number"; // default
options.Response.Pagination.PageSizeParameterName = "page-size"; // default
return ApiResults.Paginate(items, totalCount, pageNumber, pageSize);
Metadata TraceId Always Null
Problem: The metadata.traceId field is always null.
Cause: Distributed tracing is not configured, or Activity.Current is null.
Solution: Configure tracing middleware or disable the metadata block:
// Option 1: add tracing
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddAspNetCoreInstrumentation());
// Option 2: disable metadata
options.Response.IncludeMetadata = false;
Exception Details Showing in Production
Problem: Stack traces and exception details are visible in production responses.
Solution: Use null (auto-detection, recommended) or explicitly set to false in production:
options.Response.ErrorResponse.IncludeExceptionDetails = null; // default — Development only
// Explicit
options.Response.ErrorResponse.IncludeExceptionDetails =
builder.Environment.IsDevelopment();
ApiResults Methods Not Found
Problem: Compiler error: 'ApiResults' does not contain a definition for 'Ok'.
Solution: Add the correct using:
using AspNetConventions.Http;
Debugging Response Formatting
Use AfterResponseWrapAsync to inspect what the formatter sees:
options.Response.Hooks.AfterResponseWrapAsync = async (wrapped, result, request) =>
{
Console.WriteLine($"Path: {request.Path} | Status: {result.StatusCode} | ValueType: {result.GetValue()?.GetType().Name ?? "null"}");
};
Exception Handling
Custom Mapper Not Being Used
Problem: You registered a custom mapper but still get the default 500 response.
Possible causes:
- Mapper not registered:
options.ExceptionHandling.Mappers.Add(new MyMapper());
- Exception type doesn’t match — your mapper handles
OrderNotFoundExceptionbut a different type is thrown:
throw new NotFoundException("Order not found"); // not the same type
CanMapExceptionreturnsfalse— add a log to verify:
public override bool CanMapException(Exception exception, RequestDescriptor request)
{
Console.WriteLine($"CanMap: {exception.GetType().Name}");
return exception is OrderNotFoundException;
}
Multiple Mappers Matching
Problem: The wrong mapper is handling your exception.
Solution: Mappers are evaluated in registration order. Register more specific mappers first:
options.ExceptionHandling.Mappers.Add(new OrderNotFoundExceptionMapper()); // Specific
options.ExceptionHandling.Mappers.Add(new NotFoundExceptionMapper()); // General
options.ExceptionHandling.Mappers.Add(new DomainExceptionMapper()); // Base class
Exceptions Not Being Caught
Problem: Exceptions propagate without being handled by AspNetConventions.
Possible causes:
ShouldHandleAsyncreturningfalse:
options.ExceptionHandling.Hooks.ShouldHandleAsync = async (exception, request) =>
{
Console.WriteLine($"Caught: {exception.GetType().Name}");
return true;
};
Exception thrown before middleware — exceptions in
ConfigureServicesor early pipeline stages won’t be caught.Middleware order —
UseAspNetConventions()must be called before the endpoints that throw:
app.UseAuthentication();
app.UseAuthorization();
var api = app.UseAspNetConventions(); // ← here
api.MapGet("/api/orders", ...);
HTTP Status Code Defaults to 500
Problem: The response always returns 500 regardless of the exception.
Cause: StatusCode is not set in the ExceptionDescriptor.
Solution:
return new ExceptionDescriptor
{
StatusCode = HttpStatusCode.NotFound, // Must be set
Type = "NOT_FOUND",
Message = "Resource not found"
};
If StatusCode is null, the fallback behavior can be customized via ErrorResponseOptions:
options.Response.ErrorResponse.DefaultStatusCode = HttpStatusCode.InternalServerError; // default
options.Response.ErrorResponse.DefaultErrorType = "INTERNAL_ERROR";
options.Response.ErrorResponse.DefaultErrorMessage = "An error occurred.";
Conflict with Other Exception Middleware
Problem: AspNetConventions conflicts with another exception handling middleware (e.g. UseExceptionHandler).
Solution: Remove the conflicting middleware, or exclude specific exception types:
// Remove this if AspNetConventions handles exceptions
// app.UseExceptionHandler("/error");
// Or exclude specific types from AspNetConventions
options.ExceptionHandling.ExcludeStatusCodes.Add(HttpStatusCode.Unauthorized);
options.ExceptionHandling.ExcludeException.Add(typeof(SecurityException));
Debugging Exception Handling
Hook into all three stages to trace the full pipeline:
options.ExceptionHandling.Hooks.ShouldHandleAsync = async (ex, req) =>
{
Console.WriteLine($"[ExceptionHandling] Caught {ex.GetType().Name} on {req.Path}");
return true;
};
options.ExceptionHandling.Hooks.BeforeMappingAsync = async (mapper, req) =>
{
Console.WriteLine($"[ExceptionHandling] Mapper: {mapper.GetType().Name}");
return mapper;
};
options.ExceptionHandling.Hooks.AfterMappingAsync = async (descriptor, mapper, req) =>
{
Console.WriteLine($"[ExceptionHandling] Result: {descriptor.StatusCode} {descriptor.Type} | Log: {descriptor.ShouldLog}");
return descriptor;
};
JSON Serialization
Property Order Not Working
Problem: Setting .Order() on one property doesn’t produce the expected output order.
Cause: System.Text.Json only guarantees order for properties that have an explicit order value set. Properties without one are placed after all ordered properties, in their natural declaration order. If you set .Order(0) on a single property but leave the rest unordered, the result can be unexpected depending on how many unordered properties precede or follow it.
Solution A — Set order on all properties for full control:
cfg.Type<UserResponse>(type =>
{
type.Property(x => x.Id).Order(0);
type.Property(x => x.Name).Order(1);
type.Property(x => x.Email).Order(2);
type.Property(x => x.CreatedAt).Order(3);
});
Solution B — Use a negative value to push a single property to the front without touching the rest:
cfg.Type<UserResponse>(type =>
{
// Id appears first; all other properties follow in their declaration order
type.Property(x => x.Id).Order(-1);
});
CaseStyle Not Applying
Problem: Properties are still camelCase even though a different CaseStyle is set.
Cause: Each AddAspNetConventions() / UseAspNetConventions() call has its own independent options scope. If you configure CaseStyle in one call but your endpoints are registered under a different call, the setting won’t apply there.
Solution: Check that the CaseStyle is set in the correct scope. For example, these two calls are independent — a CasingStyle set in one does not affect the other:
// MVC Controllers — snake_case
builder.Services.AddControllers()
.AddAspNetConventions(o => o.Json.CaseStyle = CasingStyle.SnakeCase);
// Minimal APIs — camelCase (separate scope, separate options)
var api = app.UseAspNetConventions(o => o.Json.CaseStyle = CasingStyle.CamelCase);
Set CaseStyle in every scope where it needs to apply.
.Name() Override Still Being Transformed
Problem: A property with .Name("my_name") is still being renamed by the global CaseStyle.
Answer: Explicit .Name() values are written as-is and are never further transformed. If the output still looks wrong, verify the rule is targeting the correct type and property:
cfg.Type<Product>(type =>
{
// Confirm: is this the right type? Is "InternalSku" the exact CLR property name?
type.Property(x => x.InternalSku).Name("sku");
});
IgnoreType<T>() Not Working
Problem: Properties of type T are still appearing in the response after calling cfg.IgnoreType<T>().
Common causes:
IgnoreType<T>()is registered inside theConfigureTypesdelegate — confirm the delegate is actually assigned:
options.Json.ConfigureTypes = cfg =>
{
cfg.IgnoreType<Metadata>(); // Must be inside the delegate
};
Type mismatch —
IgnoreType<T>()walks the property’s value type chain. If the property is declared asobjector an interface, the runtime type won’t matchT. UseIgnorePropertyNamefor duck-typed properties instead.Framework default overriding —
IgnoreType<T>()has the highest priority and will override any per-type rule. If you see this failing, confirm you’re not mixing it with anIgnoreTypecall on a base type that shouldn’t apply here.
IgnorePropertyName() Not Matching
Problem: cfg.IgnorePropertyName("StatusCode") has no effect.
Cause: The lookup tries the CLR name first, then falls back to the JSON-transformed name (e.g. status_code or status-code). If neither matches, the rule is not applied.
Solution: Pass either the exact CLR name or the exact serialized name. Both forms are accepted:
cfg.IgnorePropertyName("StatusCode"); // matches CLR name
cfg.IgnorePropertyName("status_code"); // matches serialized name (snake_case)
cfg.IgnorePropertyName("statusCode"); // matches serialized name (camelCase)
Assembly Scanning Not Picking Up Configurations
Problem: A JsonTypeConfiguration<T> class exists but its rules are never applied.
Check these:
- The class is
publicand non-abstract. - The class has a public parameterless constructor.
- The correct assembly is passed to
ScanAssemblies:
// Use any type from the target assembly as the anchor
options.Json.ScanAssemblies(typeof(UserConfiguration).Assembly);
- The class actually inherits from
JsonTypeConfiguration<T>orJsonOpenGenericTypeConfiguration<T>(not a custom base in between that does not extendJsonTypeConfigurationBase).