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
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This is **Intility.JsonApiToolkit**, a .NET library that implements the JSON:API
- Microsoft.AspNetCore.Mvc
- Microsoft.EntityFrameworkCore
- Microsoft.AspNetCore.JsonPatch
- Microsoft.Extensions.DependencyInjection.Abstractions
- Intility.Logging.AspNetCore
- **Test Framework**: xUnit with Moq for mocking
- **Documentation**: DocFX for API documentation
Expand Down Expand Up @@ -83,7 +84,7 @@ Documentation is built using DocFX and deployed to GitHub Pages. The documentati

- **Convention-based mapping**: Properties are automatically mapped from C# PascalCase to JSON camelCase
- **Query parameter parsing**: Standard JSON:API query syntax (`filter[field]=value`, `sort=field,-field2`, `page[number]=1&page[size]=10`, `include=relationship`)
- **Async-first**: Main controller method `JsonApiOkAsync()` is async and works with `IQueryable<T>`
- **Async-first**: Main controller method `JsonApiQueryAsync()` is async and works with `IQueryable<T>`
- **Entity Framework integration**: Uses EF Core's `Include()` and query building capabilities
- **Filter expressions**: Complex filtering with operators (eq, ne, gt, lt, contains, etc.), logical grouping, and enum support
- **JSON column detection**: Collections and complex objects without ID properties are automatically mapped as JSON attributes instead of relationships (useful for EF Core owned entities stored as JSON columns)
Expand Down Expand Up @@ -137,7 +138,7 @@ Tests are organized by component:
### Common Patterns

- Controllers should inherit from `JsonApiController`
- Use `JsonApiOkAsync(queryable, "resourceType")` for collections with full query processing
- Use `JsonApiQueryAsync(queryable, "resourceType")` for collections with full query processing
- Use `JsonApiOk(entity, "resourceType")` for already-loaded entities or collections
- Entity types should have an `Id` property (auto-detected by `EntityMapper.GetIdProperty()`)
- Use `QueryParameters queryParams = GetJsonApiQueryParameters()` to access parsed query parameters
Expand Down
2 changes: 1 addition & 1 deletion JsonApiToolkit/Controllers/JsonApiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ protected IActionResult JsonApiOk<T>(
/// </list>
/// This is the recommended method for collection endpoints as it implements the complete JSON:API querying capabilities.
/// </remarks>
protected async Task<IActionResult> JsonApiOkAsync<T>(
protected async Task<IActionResult> JsonApiQueryAsync<T>(
IQueryable<T> queryable,
string resourceType
)
Expand Down
4 changes: 2 additions & 2 deletions JsonApiToolkit/Extensions/Querying/QueryHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static class QueryHelpers
if (property != null)
return property;

string pascalCase = StringExtensions.ToPascalCase(jsonPropertyName);
string pascalCase = jsonPropertyName.ToPascalCase();
property = entityType.GetProperty(pascalCase);

return property
Expand All @@ -57,7 +57,7 @@ public static class QueryHelpers
/// <param name="value">The string value from the query parameter</param>
/// <param name="targetType">The target property type to convert to</param>
/// <returns>
/// /// The converted value, or trows an exception if conversion fails or is not supported
/// /// The converted value, or throws an exception if conversion fails or is not supported
/// </returns>
/// <remarks>
/// Handles common primitive types (int, long, decimal, bool, DateTime, Guid, Uri, TimeSpan,
Expand Down
2 changes: 1 addition & 1 deletion JsonApiToolkit/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static string ToCamelCase(this string str)
/// Used for converting JSON property names (camelCase) to C# property names (PascalCase).
/// Returns the original string if it's null or empty.
/// </remarks>
public static string ToPascalCase(string str)
public static string ToPascalCase(this string str)
{
if (string.IsNullOrEmpty(str))
return str;
Expand Down
76 changes: 48 additions & 28 deletions JsonApiToolkit/Filters/JsonApiExceptionFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,45 +28,65 @@ public class JsonApiExceptionFilter(ILogger<JsonApiExceptionFilter> logger) : IE
/// </remarks>
public void OnException(ExceptionContext context)
{
var (status, title) = context.Exception switch
{
JsonApiBadRequestException => (400, "Bad Request"),
JsonApiNotFoundException => (404, "Not Found"),
JsonApiConflictException => (409, "Conflict"),
JsonApiUnauthorizedException => (401, "Unauthorized"),
JsonApiForbiddenException => (403, "Forbidden"),
JsonApiTooManyRequestsException => (429, "Too Many Requests"),
_ => (500, "Internal Server Error"),
};
int status;
string title;
JsonApiError error;

if (status == 500)
{
// Log full stack trace for unexpected errors
_logger.LogError(context.Exception, "An unhandled exception occurred");
}
else
if (context.Exception is JsonApiException jsonApiException)
{
// Log only the message for handled exceptions
// Handle structured JSON:API exceptions
status = jsonApiException.StatusCode;
title = GetTitleForStatusCode(status);

error = new JsonApiError
{
Status = status.ToString(),
Title = title,
Detail = jsonApiException.Message,
Code = jsonApiException.Code,
Source = jsonApiException.ErrorSource,
Meta = jsonApiException.Meta,
};

// Log handled exceptions
_logger.LogInformation(
"Handled JSON:API exception: {Type} - {Message}",
context.Exception.GetType().Name,
context.Exception.Message
jsonApiException.GetType().Name,
jsonApiException.Message
);
}

var error = new JsonApiError
else
{
Status = status.ToString(),
Title = title,
Detail =
status != 500
? context.Exception.Message
: "An error occurred while processing your request.",
};
// Handle unexpected exceptions
status = 500;
title = "Internal Server Error";

error = new JsonApiError
{
Status = status.ToString(),
Title = title,
Detail = "An error occurred while processing your request.",
};

// Log full stack trace for unexpected errors
_logger.LogError(context.Exception, "An unhandled exception occurred");
}

var response = new JsonApiErrorResponse { Errors = [error] };

context.Result = new ObjectResult(response) { StatusCode = status };
context.ExceptionHandled = true;
}

private static string GetTitleForStatusCode(int statusCode) =>
statusCode switch
{
400 => "Bad Request",
401 => "Unauthorized",
403 => "Forbidden",
404 => "Not Found",
409 => "Conflict",
429 => "Too Many Requests",
_ => "Error",
};
}
Loading