From 1c0f27a75ce1a9a83a15f7aeb310114c6ca56543 Mon Sep 17 00:00:00 2001 From: Erlend Ellefsen Date: Sat, 24 Jan 2026 14:20:03 +0100 Subject: [PATCH 1/3] chore: add CodeQL, Dependabot, release-please, and improve CI/CD pipeline --- .github/.release-please-manifest.json | 3 + .github/dependabot.yml | 44 +++ .github/release-please-config.json | 26 ++ .github/release.yml | 22 -- .github/workflows/ci-cd.yml | 97 ++++--- .github/workflows/codeql.yml | 52 ++++ .github/workflows/release-please.yml | 31 ++ JsonApiToolkit/JsonApiToolkit.csproj | 2 +- docs/docs/upgrade-guide.md | 400 ++++++++++++++++++++++++++ 9 files changed, 615 insertions(+), 62 deletions(-) create mode 100644 .github/.release-please-manifest.json create mode 100644 .github/dependabot.yml create mode 100644 .github/release-please-config.json delete mode 100644 .github/release.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 docs/docs/upgrade-guide.md 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/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..42fc04a --- /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": "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.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/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/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! +}); +``` From dcdf84249b7a770e5da433e087ba86a92bdbdb09 Mon Sep 17 00:00:00 2001 From: Erlend Ellefsen Date: Sat, 24 Jan 2026 14:21:50 +0100 Subject: [PATCH 2/3] chore: add CODEOWNERS file --- .github/CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/CODEOWNERS 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 From 0c2834831d81ebbf02c30e476bb2ad2890b8b59e Mon Sep 17 00:00:00 2001 From: Erlend Ellefsen Date: Sat, 24 Jan 2026 14:37:27 +0100 Subject: [PATCH 3/3] fix: fomatting and copilot suggestions --- .github/release-please-config.json | 2 +- ...fest.json => release-please-manifest.json} | 0 .../Extensions/FilteredIncludeBuilderTests.cs | 18 ++++++------- .../Parsing/JsonApiQueryParserTests.cs | 26 ++++++++++++------- .../Filtering/FilterExpressionBuilder.cs | 7 ++++- .../Filtering/FilterOperatorExpressions.cs | 5 ++-- .../Querying/Filtering/FilterOperator.cs | 10 +++++++ .../Querying/Filtering/LogicalOperator.cs | 2 ++ .../Models/Validation/IncludePattern.cs | 3 +++ 9 files changed, 49 insertions(+), 24 deletions(-) rename .github/{.release-please-manifest.json => release-please-manifest.json} (100%) diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 42fc04a..6bdb79c 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -3,7 +3,7 @@ "packages": { ".": { "release-type": "simple", - "package-name": "JsonApiToolkit", + "package-name": "Intility.JsonApiToolkit", "changelog-sections": [ { "type": "feat", "section": "Features", "hidden": false }, { "type": "fix", "section": "Bug Fixes", "hidden": false }, diff --git a/.github/.release-please-manifest.json b/.github/release-please-manifest.json similarity index 100% rename from .github/.release-please-manifest.json rename to .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/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, }