diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..e3dd949
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,5 @@
+# Default owner for everything
+* @erlendellefsen
+
+# Security-sensitive files require extra review
+.github/workflows/** @erlendellefsen
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..cb3522f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,44 @@
+version: 2
+
+updates:
+ # NuGet packages
+ - package-ecosystem: nuget
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ open-pull-requests-limit: 10
+ labels:
+ - dependencies
+ - nuget
+ commit-message:
+ prefix: "deps(nuget)"
+ groups:
+ # Group minor/patch updates to reduce PR noise
+ microsoft:
+ patterns:
+ - "Microsoft.*"
+ update-types:
+ - minor
+ - patch
+ testing:
+ patterns:
+ - "xunit*"
+ - "Moq*"
+ - "coverlet*"
+ update-types:
+ - minor
+ - patch
+
+ # GitHub Actions
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ day: monday
+ open-pull-requests-limit: 5
+ labels:
+ - dependencies
+ - github-actions
+ commit-message:
+ prefix: "deps(actions)"
diff --git a/.github/release-please-config.json b/.github/release-please-config.json
new file mode 100644
index 0000000..6bdb79c
--- /dev/null
+++ b/.github/release-please-config.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/refs/heads/main/schemas/config.json",
+ "packages": {
+ ".": {
+ "release-type": "simple",
+ "package-name": "Intility.JsonApiToolkit",
+ "changelog-sections": [
+ { "type": "feat", "section": "Features", "hidden": false },
+ { "type": "fix", "section": "Bug Fixes", "hidden": false },
+ { "type": "perf", "section": "Performance", "hidden": false },
+ { "type": "refactor", "section": "Refactoring", "hidden": false },
+ { "type": "docs", "section": "Documentation", "hidden": false },
+ { "type": "test", "section": "Tests", "hidden": true },
+ { "type": "chore", "section": "Maintenance", "hidden": true },
+ { "type": "deps", "section": "Dependencies", "hidden": false }
+ ],
+ "extra-files": [
+ {
+ "type": "xml",
+ "path": "JsonApiToolkit/JsonApiToolkit.csproj",
+ "xpath": "//Project/PropertyGroup/Version"
+ }
+ ]
+ }
+ }
+}
diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json
new file mode 100644
index 0000000..ecf5738
--- /dev/null
+++ b/.github/release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "1.2.5"
+}
diff --git a/.github/release.yml b/.github/release.yml
deleted file mode 100644
index 5e4fef8..0000000
--- a/.github/release.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-# .github/release.yml
-
-changelog:
- exclude:
- authors:
- - dependabot
- categories:
- - title: New Features 🎉
- labels:
- - enhancement
- - title: Bug Fixes 🐛
- labels:
- - bug
- - title: Documentation 📚
- labels:
- - docs
- - title: Dependencies 📦
- labels:
- - dependencies
- - title: Other Changes
- labels:
- - '*'
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 07167e2..2a5be5b 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -2,55 +2,74 @@ name: CI/CD Pipeline
on:
push:
- branches: [ main ]
+ branches: [main]
+ paths-ignore:
+ - "docs/**"
+ - "**.md"
+ - ".github/ISSUE_TEMPLATE/**"
+ - ".claude/**"
pull_request:
- branches: [ main ]
+ branches: [main]
+ paths-ignore:
+ - "docs/**"
+ - "**.md"
+ - ".github/ISSUE_TEMPLATE/**"
+ - ".claude/**"
release:
- types: [ published ]
+ types: [published]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
build-and-test:
runs-on: ubuntu-latest
-
+
steps:
- - uses: actions/checkout@v4
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 9.x.x
-
- - name: Restore dependencies
- run: dotnet restore
-
- - name: Build
- run: dotnet build --no-restore --configuration Release
-
- - name: Test
- run: dotnet test --no-build --configuration Release --verbosity normal
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.x
+
+ - name: Restore dependencies
+ run: dotnet restore
+
+ - name: Check formatting
+ run: |
+ dotnet tool restore
+ dotnet csharpier . --check
+
+ - name: Build
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Test
+ run: dotnet test --no-build --configuration Release --verbosity normal
publish:
needs: build-and-test
if: github.event_name == 'release'
runs-on: ubuntu-latest
-
+
steps:
- - uses: actions/checkout@v4
-
- - name: Setup .NET
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: 9.x.x
- source-url: https://nuget.pkg.github.com/Intility/index.json
- env:
- NUGET_AUTH_TOKEN: ${{ secrets.NUGET_AUTH_TOKEN }}
-
- - name: Build and Pack
- run: |
- VERSION="${{ github.event.release.tag_name }}"
- # Remove v prefix if present
- [[ "$VERSION" =~ ^v ]] && VERSION="${VERSION:1}"
- dotnet pack JsonApiToolkit/JsonApiToolkit.csproj -p:PackageVersion=$VERSION -c Release
-
- - name: Publish to GitHub Packages
- run: dotnet nuget push "JsonApiToolkit/bin/Release/*.nupkg" --api-key ${{ secrets.NUGET_AUTH_TOKEN }}
+ - uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.x
+ source-url: https://nuget.pkg.github.com/Intility/index.json
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.NUGET_AUTH_TOKEN }}
+
+ - name: Build and Pack
+ run: |
+ VERSION="${{ github.event.release.tag_name }}"
+ # Remove v prefix if present
+ [[ "$VERSION" =~ ^v ]] && VERSION="${VERSION:1}"
+ dotnet pack JsonApiToolkit/JsonApiToolkit.csproj -p:PackageVersion=$VERSION -c Release
+
+ - name: Publish to GitHub Packages
+ run: dotnet nuget push "JsonApiToolkit/bin/Release/*.nupkg" --api-key ${{ secrets.NUGET_AUTH_TOKEN }}
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..72a9669
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,52 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - "**.cs"
+ - "**.csproj"
+ pull_request:
+ branches: [main]
+ paths:
+ - "**.cs"
+ - "**.csproj"
+ schedule:
+ - cron: "0 6 * * 1" # Monday 6am UTC
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ packages: read
+ actions: read
+ contents: read
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.x
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: csharp
+ queries: security-and-quality
+
+ - name: Build
+ run: dotnet build --configuration Release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:csharp"
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
new file mode 100644
index 0000000..37b6ed6
--- /dev/null
+++ b/.github/workflows/release-please.yml
@@ -0,0 +1,31 @@
+name: Release Please
+
+permissions:
+ contents: write
+ pull-requests: write
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ release-please:
+ if: github.event_name == 'push'
+ name: 🚀 Release Please
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf
+ id: app-token
+ with:
+ app-id: ${{ vars.RELEASE_BOT_APP_ID }}
+ private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
+
+ - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38
+ with:
+ token: ${{ steps.app-token.outputs.token }}
+ config-file: .github/release-please-config.json
+ manifest-file: .github/release-please-manifest.json
diff --git a/JsonApiToolkit.Tests/Extensions/FilteredIncludeBuilderTests.cs b/JsonApiToolkit.Tests/Extensions/FilteredIncludeBuilderTests.cs
index 7a3e72e..0bc1b7f 100644
--- a/JsonApiToolkit.Tests/Extensions/FilteredIncludeBuilderTests.cs
+++ b/JsonApiToolkit.Tests/Extensions/FilteredIncludeBuilderTests.cs
@@ -74,9 +74,9 @@ public void ApplyFilteredIncludes_WithSimpleIncludeFilter_BuildsCorrectExpressio
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
- }
- }
- }
+ },
+ },
+ },
},
};
@@ -114,9 +114,9 @@ public void ApplyFilteredIncludes_WithMultipleFiltersOnSameRelationship_Combines
Field = "priority",
Operator = FilterOperator.Gt,
Value = "5",
- }
- }
- }
+ },
+ },
+ },
},
};
@@ -162,9 +162,9 @@ public void ApplyFilteredIncludes_WithMixedFilteredAndUnfilteredIncludes_Handles
Field = "status",
Operator = FilterOperator.Eq,
Value = "approved",
- }
- }
- }
+ },
+ },
+ },
},
// tags and author have no filters
};
diff --git a/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs b/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs
index 1a3bef4..0d08fe2 100644
--- a/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs
+++ b/JsonApiToolkit.Tests/Parsing/JsonApiQueryParserTests.cs
@@ -303,11 +303,14 @@ public void Parse_WithOrChainDotNotation_CreatesPrimaryFilters()
Assert.Equal(2, orGroup.Filters.Count);
// Both should be primary filters (dot notation)
- Assert.All(orGroup.Filters, f =>
- {
- Assert.Equal("vulnerability.severity", f.Field);
- Assert.False(f.IsIncludeFilter);
- });
+ Assert.All(
+ orGroup.Filters,
+ f =>
+ {
+ Assert.Equal("vulnerability.severity", f.Field);
+ Assert.False(f.IsIncludeFilter);
+ }
+ );
}
[Fact]
@@ -333,11 +336,14 @@ public void Parse_WithOrChainBracketSyntax_CreatesIncludeFilters()
Assert.Equal(2, orGroup.Filters.Count);
// Both should be include filters (bracket syntax)
- Assert.All(orGroup.Filters, f =>
- {
- Assert.Equal("vulnerability.severity", f.Field);
- Assert.True(f.IsIncludeFilter);
- });
+ Assert.All(
+ orGroup.Filters,
+ f =>
+ {
+ Assert.Equal("vulnerability.severity", f.Field);
+ Assert.True(f.IsIncludeFilter);
+ }
+ );
}
[Fact]
diff --git a/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs b/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs
index 51c6bb5..53c855c 100644
--- a/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs
+++ b/JsonApiToolkit/Extensions/Querying/Filtering/FilterExpressionBuilder.cs
@@ -71,7 +71,12 @@ public static class FilterExpressionBuilder
foreach (FilterGroup nestedGroup in group.Groups)
{
- Expression? nestedExpr = BuildFilterExpression(nestedGroup, parameter, entityType, logger);
+ Expression? nestedExpr = BuildFilterExpression(
+ nestedGroup,
+ parameter,
+ entityType,
+ logger
+ );
if (nestedExpr != null)
expressions.Add(nestedExpr);
}
diff --git a/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs b/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs
index d118225..7230040 100644
--- a/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs
+++ b/JsonApiToolkit/Extensions/Querying/Filtering/FilterOperatorExpressions.cs
@@ -10,9 +10,8 @@ internal static Expression BuildLikeExpression(Expression property, string value
{
// Only strip % if value has both leading AND trailing % (indicating wildcard intent)
// This preserves literal % in values like "100%" or "%discount"
- string cleanValue = value.StartsWith('%') && value.EndsWith('%') && value.Length > 2
- ? value[1..^1]
- : value;
+ string cleanValue =
+ value.StartsWith('%') && value.EndsWith('%') && value.Length > 2 ? value[1..^1] : value;
if (property.Type == typeof(string))
{
diff --git a/JsonApiToolkit/JsonApiToolkit.csproj b/JsonApiToolkit/JsonApiToolkit.csproj
index a79619f..165dd4e 100644
--- a/JsonApiToolkit/JsonApiToolkit.csproj
+++ b/JsonApiToolkit/JsonApiToolkit.csproj
@@ -7,7 +7,7 @@
Intility.JsonApiToolkit
- 1.1.22-local
+ 1.2.5
Intility
Intility
A toolkit for implementing JSON:API specification in .NET applications
diff --git a/JsonApiToolkit/Models/Querying/Filtering/FilterOperator.cs b/JsonApiToolkit/Models/Querying/Filtering/FilterOperator.cs
index 13aa4d9..8a48dda 100644
--- a/JsonApiToolkit/Models/Querying/Filtering/FilterOperator.cs
+++ b/JsonApiToolkit/Models/Querying/Filtering/FilterOperator.cs
@@ -7,24 +7,34 @@ public enum FilterOperator
{
/// Equal to.
Eq,
+
/// Not equal to.
Ne,
+
/// Greater than.
Gt,
+
/// Greater than or equal to.
Ge,
+
/// Less than.
Lt,
+
/// Less than or equal to.
Le,
+
/// String contains (case-insensitive).
Like,
+
/// In list of values.
In,
+
/// Not in list of values.
Nin,
+
/// Is null.
IsNull,
+
/// Is not null.
IsNotNull,
}
diff --git a/JsonApiToolkit/Models/Querying/Filtering/LogicalOperator.cs b/JsonApiToolkit/Models/Querying/Filtering/LogicalOperator.cs
index 7f11255..8f38de2 100644
--- a/JsonApiToolkit/Models/Querying/Filtering/LogicalOperator.cs
+++ b/JsonApiToolkit/Models/Querying/Filtering/LogicalOperator.cs
@@ -7,8 +7,10 @@ public enum LogicalOperator
{
/// All conditions must be true.
And,
+
/// At least one condition must be true.
Or,
+
/// Negates the condition.
Not,
}
diff --git a/JsonApiToolkit/Models/Validation/IncludePattern.cs b/JsonApiToolkit/Models/Validation/IncludePattern.cs
index dd67302..c7193ba 100644
--- a/JsonApiToolkit/Models/Validation/IncludePattern.cs
+++ b/JsonApiToolkit/Models/Validation/IncludePattern.cs
@@ -155,10 +155,13 @@ public enum PatternType
{
/// Exact match with no wildcards.
Exact,
+
/// Top-level wildcard (*).
TopLevelWildcard,
+
/// Single-level wildcard (e.g., author.*).
SingleLevelWildcard,
+
/// Complex wildcard pattern.
ComplexWildcard,
}
diff --git a/docs/docs/upgrade-guide.md b/docs/docs/upgrade-guide.md
new file mode 100644
index 0000000..8efdb2f
--- /dev/null
+++ b/docs/docs/upgrade-guide.md
@@ -0,0 +1,400 @@
+# Upgrade Guide
+
+This document tracks all breaking changes, new features, and migration steps for each version of JsonApiToolkit.
+
+**Current Version:** 1.2.5
+
+---
+
+## .NET 10 Upgrade (November 2025)
+
+**Target Version:** 2.3.0 or 3.0.0 (TBD)
+
+**Timeline:** After .NET 10 GA release (November 2025)
+
+**Changes:**
+- [ ] Update target framework from `net9.0` to `net10.0`
+- [ ] Update Microsoft.* dependencies to .NET 10 versions
+- [ ] Review and adopt new C# 14 language features where beneficial
+- [ ] Update GitHub Actions to use `dotnet-version: 10.x`
+- [ ] Update documentation prerequisites
+
+**Breaking Changes:**
+- Minimum runtime requirement changes from .NET 9 to .NET 10
+- Applications must upgrade to .NET 10 to use new package versions
+
+**Migration:**
+1. Update your project to target .NET 10
+2. Update JsonApiToolkit package to the new version
+
+**Multi-targeting consideration:**
+If there's demand, we may multi-target `net9.0;net10.0` for one release cycle to ease migration.
+
+---
+
+## Version 2.x (Upcoming - Breaking Changes)
+
+### v2.2.0 - Database Projection
+
+**Release Date:** TBD
+
+**New Features:**
+- [ ] Database-level projection - only fetch requested columns from database
+- [ ] Opt-in via `EnableDatabaseProjection` option
+- [ ] Massive performance improvement for entities with JSON columns
+
+**Configuration:**
+```csharp
+services.AddJsonApiToolkit(options => {
+ options.EnableDatabaseProjection = true;
+});
+```
+
+**Breaking Changes:** None (opt-in feature)
+
+**Migration:** None required
+
+---
+
+### v2.1.0 - Sparse Fieldsets
+
+**Release Date:** TBD
+
+**New Features:**
+- [ ] `fields[type]` query parameter support (JSON:API sparse fieldsets)
+- [ ] Reduces response payload size by returning only requested attributes
+- [ ] Works with included resources: `fields[author]=name,email`
+
+**Usage:**
+```
+GET /articles?fields[articles]=title,body&include=author&fields[author]=name
+```
+
+**Response:**
+```json
+{
+ "data": {
+ "type": "articles",
+ "id": "1",
+ "attributes": {
+ "title": "Hello World",
+ "body": "..."
+ }
+ },
+ "included": [{
+ "type": "author",
+ "id": "5",
+ "attributes": {
+ "name": "John Doe"
+ }
+ }]
+}
+```
+
+**Breaking Changes:** None
+
+**Migration:** None required
+
+---
+
+### v2.0.0 - Architecture Refactor (MAJOR BREAKING CHANGE)
+
+**Release Date:** TBD
+
+**New Features:**
+- [ ] Full dependency injection support
+- [ ] All core components now injectable and mockable
+- [ ] Interfaces for all handlers (`IFilterHandler`, `ISortHandler`, etc.)
+- [ ] Extended `JsonApiOptions` (introduced in v1.4.0) with more configuration
+
+**Breaking Changes:**
+
+#### 1. Controller Constructor Signature Changed
+
+**Before (v1.x):**
+```csharp
+public class BooksController : JsonApiController
+{
+ private readonly AppDbContext _db;
+
+ public BooksController(AppDbContext db)
+ {
+ _db = db;
+ }
+}
+```
+
+**After (v2.0):**
+```csharp
+public class BooksController : JsonApiController
+{
+ private readonly AppDbContext _db;
+
+ public BooksController(
+ AppDbContext db,
+ ILogger logger,
+ IJsonApiMapper mapper,
+ IJsonApiQueryParser queryParser,
+ IOptions options)
+ : base(logger, mapper, queryParser, options)
+ {
+ _db = db;
+ }
+}
+```
+
+#### 2. Static Extension Methods Changed
+
+Most users won't notice this change - the base controller methods (`JsonApiQueryAsync`, `JsonApiOk`, etc.) continue to work as before. However, if you were calling static methods directly:
+
+**Before (v1.x):**
+```csharp
+var document = JsonApiMapper.ToDocument(entity, "books");
+```
+
+**After (v2.0):**
+```csharp
+// Use the inherited Mapper property from JsonApiController
+var document = Mapper.ToDocument(entity, "books");
+```
+
+**For advanced usage** - if you need direct handler access outside controller methods:
+```csharp
+public class BooksController : JsonApiController
+{
+ private readonly IFilterHandler _filterHandler;
+
+ public BooksController(
+ AppDbContext db,
+ IFilterHandler filterHandler, // Inject directly if needed
+ ILogger logger,
+ IJsonApiMapper mapper,
+ IJsonApiQueryParser queryParser,
+ IOptions options)
+ : base(logger, mapper, queryParser, options)
+ {
+ _db = db;
+ _filterHandler = filterHandler;
+ }
+}
+```
+
+#### 3. New Service Registrations
+
+All services are now automatically registered by `AddJsonApiToolkit()`, but if you were manually resolving services, the types have changed:
+
+| Before (v1.x) | After (v2.0) |
+|---------------|--------------|
+| `JsonApiMapper` (static) | `IJsonApiMapper` (scoped) |
+| `EntityMapper` (static) | `IEntityMapper` (scoped) |
+| N/A | `IFilterHandler` (scoped) |
+| N/A | `ISortHandler` (scoped) |
+| N/A | `IPaginationHandler` (scoped) |
+| N/A | `IIncludeHandler` (scoped) |
+
+**Migration Steps:**
+
+1. Update all controller constructors to accept required dependencies
+2. Call `base(...)` with the new parameters
+3. Replace static method calls with injected service calls
+4. Update any manual service resolutions
+
+**Compatibility Helper (Deprecated):**
+
+For easier migration, v2.0 includes deprecated static wrappers that will be removed in v3.0:
+
+```csharp
+// These work but emit deprecation warnings
+[Obsolete("Use IJsonApiMapper instead")]
+public static class JsonApiMapper { ... }
+```
+
+---
+
+## Version 1.x
+
+### v1.5.0 - Test Coverage
+
+**Release Date:** TBD
+
+**Changes:**
+- [ ] Comprehensive test suite added
+- [ ] No API changes
+
+**Breaking Changes:** None
+
+---
+
+### v1.4.0 - Security Hardening
+
+**Release Date:** TBD
+
+**New Features:**
+- [ ] `JsonApiOptions` configuration class
+- [ ] Configurable query complexity limits
+- [ ] AllowedIncludes now validates filter paths (not just includes)
+
+**Configuration:**
+```csharp
+services.AddJsonApiToolkit(options => {
+ options.MaxFilters = 50; // Default: 50
+ options.MaxFilterGroups = 10; // Default: 10
+ options.MaxFilterDepth = 3; // Default: 3
+ options.MaxFilterValueLength = 1000; // Default: 1000
+ options.MaxIncludeDepth = 4; // Default: 4
+ options.MaxPageSize = 100; // Default: 100
+ options.DefaultPageSize = 10; // Default: 10
+});
+```
+
+**Breaking Changes:**
+
+#### 1. Query Complexity Limits Enforced
+
+Queries exceeding limits now return 400 Bad Request:
+
+```json
+{
+ "errors": [{
+ "status": "400",
+ "title": "Bad Request",
+ "detail": "Query exceeds maximum filter count of 50"
+ }]
+}
+```
+
+**Migration:** If your application uses complex queries, increase limits via configuration.
+
+#### 2. Filter Path Validation with AllowedIncludes
+
+Dot-notation filters are now validated against `AllowedIncludes`:
+
+**Before (v1.3):**
+```csharp
+[AllowedIncludes("profile")]
+public async Task GetUsers()
+{
+ // filter[admin.password][like]=% would work (security hole!)
+}
+```
+
+**After (v1.4):**
+```csharp
+[AllowedIncludes("profile")]
+public async Task GetUsers()
+{
+ // filter[admin.password][like]=% returns 403 Forbidden
+}
+```
+
+**Migration:** Add relationships to `AllowedIncludes` if you need to filter on them.
+
+---
+
+### v1.3.0 - Bug Fixes & Error Improvements
+
+**Release Date:** TBD
+
+**Bug Fixes:**
+- [ ] Fixed exception swallowing in InclusionMapper (now properly logged)
+- [ ] Fixed unsafe string parsing in filter parser
+- [ ] Fixed potential division by zero in pagination
+- [ ] Added defensive checks for reflection method lookups
+- [ ] Removed dead code (`AddIncludedResourcesRecursive`)
+
+**New Features:**
+- [ ] `JsonApiErrorCodes` - Standard error codes for consistent error identification
+- [ ] `JsonApiErrors` - Factory methods for creating rich, well-structured errors
+
+**Usage:**
+```csharp
+// Before - verbose, missing metadata
+throw new JsonApiNotFoundException("Book not found");
+
+// After - concise, consistent, includes metadata
+throw JsonApiErrors.NotFound("books", id);
+```
+
+Produces:
+```json
+{
+ "errors": [{
+ "status": "404",
+ "code": "RESOURCE_NOT_FOUND",
+ "title": "Not Found",
+ "detail": "Resource 'books' with id '123' not found",
+ "meta": {
+ "resourceType": "books",
+ "id": "123"
+ }
+ }]
+}
+```
+
+**Available Factories:**
+| Factory | Status | Use Case |
+|---------|--------|----------|
+| `JsonApiErrors.NotFound(type, id)` | 404 | Resource not found |
+| `JsonApiErrors.InvalidFilterValue(field, value, type)` | 400 | Type conversion failed |
+| `JsonApiErrors.InvalidFilterField(field, entityType)` | 400 | Field doesn't exist |
+| `JsonApiErrors.IncludeNotAllowed(include)` | 403 | Include blocked by AllowedIncludes |
+| `JsonApiErrors.AlreadyExists(type, field, value)` | 409 | Duplicate key violation |
+| `JsonApiErrors.ValidationFailed(field, message)` | 400 | Generic validation error |
+
+**Breaking Changes:** None
+
+**Migration:** None required (existing exception classes still work)
+
+---
+
+### v1.2.5 - Current Release
+
+Current stable version.
+
+---
+
+## Deprecation Schedule
+
+| Feature | Deprecated In | Removed In | Replacement |
+|---------|---------------|------------|-------------|
+| Static `JsonApiMapper` class | v2.0.0 | v3.0.0 | `IJsonApiMapper` service |
+| Static `EntityMapper` class | v2.0.0 | v3.0.0 | `IEntityMapper` service |
+| Static extension methods | v2.0.0 | v3.0.0 | Injected handler services |
+
+---
+
+## FAQ
+
+### Q: Do I need to update all my controllers at once for v2.0?
+
+No. The deprecated static methods will continue to work in v2.0 (with warnings). You can migrate controllers incrementally. However, they will be removed in v3.0.
+
+### Q: Will sparse fieldsets slow down my API?
+
+In v2.1, sparse fieldsets only filter at serialization time - the database still loads all columns. In v2.2 with projection enabled, the database query itself is optimized.
+
+### Q: How do I know if my queries exceed the new limits?
+
+Enable debug logging for `JsonApiToolkit` to see query complexity metrics:
+
+```json
+{
+ "Logging": {
+ "LogLevel": {
+ "JsonApiToolkit": "Debug"
+ }
+ }
+}
+```
+
+### Q: Can I disable the new security limits?
+
+Yes, set them to high values:
+
+```csharp
+services.AddJsonApiToolkit(options => {
+ options.MaxFilters = int.MaxValue;
+ options.MaxFilterDepth = int.MaxValue;
+ // Not recommended for production!
+});
+```