Skip to content

Commit 945ab50

Browse files
Merge pull request #18 from intility/17-v1
2 parents bfe7635 + fec75f5 commit 945ab50

11 files changed

Lines changed: 345 additions & 54 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ This is **Intility.JsonApiToolkit**, a .NET library that implements the JSON:API
1313
- Microsoft.AspNetCore.Mvc
1414
- Microsoft.EntityFrameworkCore
1515
- Microsoft.AspNetCore.JsonPatch
16+
- Microsoft.Extensions.DependencyInjection.Abstractions
1617
- Intility.Logging.AspNetCore
1718
- **Test Framework**: xUnit with Moq for mocking
1819
- **Documentation**: DocFX for API documentation
@@ -83,7 +84,7 @@ Documentation is built using DocFX and deployed to GitHub Pages. The documentati
8384

8485
- **Convention-based mapping**: Properties are automatically mapped from C# PascalCase to JSON camelCase
8586
- **Query parameter parsing**: Standard JSON:API query syntax (`filter[field]=value`, `sort=field,-field2`, `page[number]=1&page[size]=10`, `include=relationship`)
86-
- **Async-first**: Main controller method `JsonApiOkAsync()` is async and works with `IQueryable<T>`
87+
- **Async-first**: Main controller method `JsonApiQueryAsync()` is async and works with `IQueryable<T>`
8788
- **Entity Framework integration**: Uses EF Core's `Include()` and query building capabilities
8889
- **Filter expressions**: Complex filtering with operators (eq, ne, gt, lt, contains, etc.), logical grouping, and enum support
8990
- **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)
@@ -137,7 +138,7 @@ Tests are organized by component:
137138
### Common Patterns
138139

139140
- Controllers should inherit from `JsonApiController`
140-
- Use `JsonApiOkAsync(queryable, "resourceType")` for collections with full query processing
141+
- Use `JsonApiQueryAsync(queryable, "resourceType")` for collections with full query processing
141142
- Use `JsonApiOk(entity, "resourceType")` for already-loaded entities or collections
142143
- Entity types should have an `Id` property (auto-detected by `EntityMapper.GetIdProperty()`)
143144
- Use `QueryParameters queryParams = GetJsonApiQueryParameters()` to access parsed query parameters

JsonApiToolkit/Controllers/JsonApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ protected IActionResult JsonApiOk<T>(
132132
/// </list>
133133
/// This is the recommended method for collection endpoints as it implements the complete JSON:API querying capabilities.
134134
/// </remarks>
135-
protected async Task<IActionResult> JsonApiOkAsync<T>(
135+
protected async Task<IActionResult> JsonApiQueryAsync<T>(
136136
IQueryable<T> queryable,
137137
string resourceType
138138
)

JsonApiToolkit/Extensions/Querying/QueryHelpers.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public static class QueryHelpers
4040
if (property != null)
4141
return property;
4242

43-
string pascalCase = StringExtensions.ToPascalCase(jsonPropertyName);
43+
string pascalCase = jsonPropertyName.ToPascalCase();
4444
property = entityType.GetProperty(pascalCase);
4545

4646
return property
@@ -57,7 +57,7 @@ public static class QueryHelpers
5757
/// <param name="value">The string value from the query parameter</param>
5858
/// <param name="targetType">The target property type to convert to</param>
5959
/// <returns>
60-
/// /// The converted value, or trows an exception if conversion fails or is not supported
60+
/// /// The converted value, or throws an exception if conversion fails or is not supported
6161
/// </returns>
6262
/// <remarks>
6363
/// Handles common primitive types (int, long, decimal, bool, DateTime, Guid, Uri, TimeSpan,

JsonApiToolkit/Extensions/StringExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static string ToCamelCase(this string str)
3434
/// Used for converting JSON property names (camelCase) to C# property names (PascalCase).
3535
/// Returns the original string if it's null or empty.
3636
/// </remarks>
37-
public static string ToPascalCase(string str)
37+
public static string ToPascalCase(this string str)
3838
{
3939
if (string.IsNullOrEmpty(str))
4040
return str;

JsonApiToolkit/Filters/JsonApiExceptionFilter.cs

Lines changed: 48 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,45 +28,65 @@ public class JsonApiExceptionFilter(ILogger<JsonApiExceptionFilter> logger) : IE
2828
/// </remarks>
2929
public void OnException(ExceptionContext context)
3030
{
31-
var (status, title) = context.Exception switch
32-
{
33-
JsonApiBadRequestException => (400, "Bad Request"),
34-
JsonApiNotFoundException => (404, "Not Found"),
35-
JsonApiConflictException => (409, "Conflict"),
36-
JsonApiUnauthorizedException => (401, "Unauthorized"),
37-
JsonApiForbiddenException => (403, "Forbidden"),
38-
JsonApiTooManyRequestsException => (429, "Too Many Requests"),
39-
_ => (500, "Internal Server Error"),
40-
};
31+
int status;
32+
string title;
33+
JsonApiError error;
4134

42-
if (status == 500)
43-
{
44-
// Log full stack trace for unexpected errors
45-
_logger.LogError(context.Exception, "An unhandled exception occurred");
46-
}
47-
else
35+
if (context.Exception is JsonApiException jsonApiException)
4836
{
49-
// Log only the message for handled exceptions
37+
// Handle structured JSON:API exceptions
38+
status = jsonApiException.StatusCode;
39+
title = GetTitleForStatusCode(status);
40+
41+
error = new JsonApiError
42+
{
43+
Status = status.ToString(),
44+
Title = title,
45+
Detail = jsonApiException.Message,
46+
Code = jsonApiException.Code,
47+
Source = jsonApiException.ErrorSource,
48+
Meta = jsonApiException.Meta,
49+
};
50+
51+
// Log handled exceptions
5052
_logger.LogInformation(
5153
"Handled JSON:API exception: {Type} - {Message}",
52-
context.Exception.GetType().Name,
53-
context.Exception.Message
54+
jsonApiException.GetType().Name,
55+
jsonApiException.Message
5456
);
5557
}
56-
57-
var error = new JsonApiError
58+
else
5859
{
59-
Status = status.ToString(),
60-
Title = title,
61-
Detail =
62-
status != 500
63-
? context.Exception.Message
64-
: "An error occurred while processing your request.",
65-
};
60+
// Handle unexpected exceptions
61+
status = 500;
62+
title = "Internal Server Error";
63+
64+
error = new JsonApiError
65+
{
66+
Status = status.ToString(),
67+
Title = title,
68+
Detail = "An error occurred while processing your request.",
69+
};
70+
71+
// Log full stack trace for unexpected errors
72+
_logger.LogError(context.Exception, "An unhandled exception occurred");
73+
}
6674

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

6977
context.Result = new ObjectResult(response) { StatusCode = status };
7078
context.ExceptionHandled = true;
7179
}
80+
81+
private static string GetTitleForStatusCode(int statusCode) =>
82+
statusCode switch
83+
{
84+
400 => "Bad Request",
85+
401 => "Unauthorized",
86+
403 => "Forbidden",
87+
404 => "Not Found",
88+
409 => "Conflict",
89+
429 => "Too Many Requests",
90+
_ => "Error",
91+
};
7292
}
File renamed without changes.

0 commit comments

Comments
 (0)