JsonApiKit.EntityFrameworkCore 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package JsonApiKit.EntityFrameworkCore --version 1.0.0
                    
NuGet\Install-Package JsonApiKit.EntityFrameworkCore -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JsonApiKit.EntityFrameworkCore" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JsonApiKit.EntityFrameworkCore" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="JsonApiKit.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JsonApiKit.EntityFrameworkCore --version 1.0.0
                    
#r "nuget: JsonApiKit.EntityFrameworkCore, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JsonApiKit.EntityFrameworkCore@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JsonApiKit.EntityFrameworkCore&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=JsonApiKit.EntityFrameworkCore&version=1.0.0
                    
Install as a Cake Tool

JsonApiKit

Lightweight JSON:API library for .NET — drop-in resource shapes, pagination, error handling, and query parsing without owning your app architecture.

JsonApiKit gives you the building blocks to produce JSON:API-compliant (or relaxed) responses from any .NET API. It does not take over your routing, controllers, or data access — you stay in control while getting consistent, well-structured output.

Table of Contents

Packages

Package Description Target
JsonApiKit Core types — documents, resources, pagination, errors, serialization. Zero ASP.NET dependency. netstandard2.1, net8.0, net9.0
JsonApiKit.AspNetCore ASP.NET Core integration — result extensions, exception handling, validation filters, DI registration. net8.0
JsonApiKit.EntityFrameworkCore EF Core integration — query pipeline, cursor/offset pagination, paged responses. net8.0
JsonApiKit.SourceGenerators Roslyn source generators — reduce resource boilerplate with attributes. netstandard2.0

Installation

# Core (required)
dotnet add package JsonApiKit

# ASP.NET Core integration
dotnet add package JsonApiKit.AspNetCore

# EF Core query pipeline
dotnet add package JsonApiKit.EntityFrameworkCore

# Source generators (optional)
dotnet add package JsonApiKit.SourceGenerators

Quick Start

using JsonApiKit.AspNetCore.DependencyInjection;
using JsonApiKit.AspNetCore.Results;
using JsonApiKit.Configuration;
using JsonApiKit.Includes;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddJsonApiKit(options =>
{
    options.Mode = JsonApiMode.Relaxed;
    options.DefaultPageSize = 10;
    options.MaxPageSize = 50;
    options.DefaultPaginationType = PaginationType.Cursor;
    options.IncludeExceptionDetails = builder.Environment.IsDevelopment();
});

var app = builder.Build();

app.UseExceptionHandler(o => { });

app.MapGet("/api/v1/books/{id:int}", async (int id, MyDbContext db, string? include) =>
{
    var includeCtx = IncludeParser.Parse(include);
    var book = await db.Books.FindAsync(id);

    if (book is null)
        return "Book not found".ToJsonApiError("NOT_FOUND", 404);

    return new BookResource(book, includeCtx).ToJsonApiResult("BOOK_FETCHED");
});

app.Run();

Core Concepts

Documents

All responses are wrapped in a document type:

// Success response
JsonApiDocument<TData>
{
    ResponseCode,        // application-level code, e.g. "BOOKS_FETCHED"
    ResponseMessage,     // human-readable message
    HttpCode,            // HTTP status code
    Data,                // the payload
    Errors,              // optional error list
    Meta,                // optional metadata dictionary
    Links,               // optional links
    Included             // optional sideloaded resources
}

// Error response
JsonApiErrorDocument
{
    ResponseCode,
    ResponseMessage,
    HttpCode,
    Errors,              // required error list
    Meta
}

Resources

Implement IJsonApiResource to define a resource shape:

public class BookResource : IJsonApiResource
{
    public string Id { get; set; } = "";
    public string Type => "book";
    public BookAttributes Attributes { get; set; } = null!;
    public BookRelationships? Relationships { get; set; }

    public BookResource(Book book, IncludeContext ctx)
    {
        Id = book.Id.ToString();
        Attributes = new BookAttributes(book.Title, book.Slug);
        Relationships = ctx.ShouldInclude("author") && book.Author is not null
            ? new BookRelationships(new AuthorResource(book.Author, ctx.Descend("author")))
            : null;
    }
}

public record BookAttributes(string Title, string Slug);
public record BookRelationships(AuthorResource? Author);

Typed variants are also available:

  • IJsonApiResource<TAttributes> — resource with typed attributes
  • IJsonApiResource<TAttributes, TRelationships> — resource with typed attributes and relationships

Relationships

For strict-mode relationship serialization:

// To-one: serializes as { "data": { "id": "1", "type": "author" } }
Relationship<AuthorResource>

// To-many: serializes as { "data": [{ "id": "1", "type": "book" }, ...] }
RelationshipCollection<BookResource>

// Resource identifier
ResourceIdentifier(string Id, string Type)

// Strict relationship records
RelationshipToOne(ResourceIdentifier? Data)
RelationshipToMany(IReadOnlyList<ResourceIdentifier>? Data)

The IncludedCollector deduplicates resources by (Type, Id) for the top-level included array.

Errors

public record JsonApiError
{
    public required string Code { get; init; }     // e.g. "VALIDATION_ERROR"
    public string? Detail { get; init; }            // human-readable detail
    public string? Status { get; init; }            // HTTP status as string
    public JsonApiErrorSource? Source { get; init; } // pointer + parameter
    public JsonApiMeta? Meta { get; init; }
}

public record JsonApiErrorSource
{
    public string? Pointer { get; init; }           // e.g. "/data/attributes/title"
    public string? Parameter { get; init; }         // e.g. "filter"
}

Pagination

Two pagination strategies with polymorphic metadata:

Offset pagination:

PaginationMeta.Offset(new OffsetPage(
    Current: 1, Size: 10, Total: 42, TotalPages: 5,
    HasNext: true, HasPrevious: false, From: 1, To: 10
))

Cursor pagination:

PaginationMeta.Cursor(new CursorPage(
    Size: 10,
    Next: "base64-encoded-cursor",
    Previous: null
))

PaginationCursor<TId> provides Base64URL-encoded cursors containing timestamp + ID for stable, keyset-based pagination.

ResourceLinks(string Self)                         // per-resource self link
PaginationLinks(string? First, string? Last,       // page navigation
                string? Prev, string? Next)
JsonApiLinks { Self, Related, Pagination }         // document-level links
JsonApiMeta : Dictionary<string, object?>          // arbitrary metadata

Configuration

builder.Services.AddJsonApiKit(options =>
{
    options.Mode = JsonApiMode.Relaxed;         // Relaxed (default) or Strict
    options.NamingPolicy = JsonNamingPolicy.CamelCase;
    options.DefaultPageSize = 10;
    options.MaxPageSize = 100;
    options.DefaultPaginationType = PaginationType.Cursor;
    options.IncludeLinks = false;
    options.BaseUrl = "https://api.example.com";
    options.IncludeExceptionDetails = false;    // true in dev for stack traces
});

Relaxed vs Strict Mode

Relaxed mode (default) — relationships are serialized as full resource objects inline:

{
  "data": {
    "id": "1",
    "type": "book",
    "attributes": { "title": "1984" },
    "relationships": {
      "author": { "id": "1", "type": "author", "attributes": { "name": "George Orwell" } }
    }
  }
}

Strict mode — follows the JSON:API specification exactly. Relationships contain only resource linkage; full resources go into a top-level included array:

{
  "data": {
    "id": "1",
    "type": "book",
    "attributes": { "title": "1984" },
    "relationships": {
      "author": { "data": { "id": "1", "type": "author" } }
    }
  },
  "included": [
    { "id": "1", "type": "author", "attributes": { "name": "George Orwell" } }
  ]
}

Include Depth Tracking

IncludeContext is the key mechanism for controlling which relationships get loaded and how deep the inclusion chain goes. It prevents N+1 issues and circular reference bloat.

// Parse the ?include= query parameter
var ctx = IncludeParser.Parse("author,author.books", maxAllowedDepth: 3);

// Check if a relationship should be included at the current level
ctx.ShouldInclude("author");  // true

// Descend into a relationship (increments depth, updates path)
var authorCtx = ctx.Descend("author");
authorCtx.ShouldInclude("books");  // true (matches "author.books")

// Further nesting respects max depth
var booksCtx = authorCtx.Descend("books");
booksCtx.ShouldInclude("author"); // false (depth limit reached)

Usage in resource constructors:

public BookResource(Book book, IncludeContext ctx)
{
    Id = book.Id.ToString();
    Attributes = new BookAttributes(book.Title, book.Slug);

    if (ctx.ShouldInclude("author") && book.Author is not null)
        Relationships = new BookRelationships(
            new AuthorResource(book.Author, ctx.Descend("author")));
}

IncludeParser.Parse() is case-insensitive, supports comma-separated dot-notation paths, and clamps to a configurable maximum depth (default 3).

Sparse Fieldsets

SparseFieldsetContext parses the fields[type]=field1,field2 query string pattern:

var fieldsetCtx = SparseFieldsetContext.Parse(httpContext.Request.QueryString.Value);

// Check if a specific field should be included for a type
fieldsetCtx.ShouldIncludeField("book", "title");  // true or false

// Get all requested fields for a type
HashSet<string>? fields = fieldsetCtx.GetFieldsFor("book");

Sparse fieldsets are not enforced at the serialization layer — they are designed to be checked during resource mapping so you have full control over field filtering.

ASP.NET Core Integration

DI Registration

builder.Services.AddJsonApiKit(options =>
{
    options.Mode = JsonApiMode.Relaxed;
    // ...
});

This registers IOptions<JsonApiOptions>, a configured JsonSerializerOptions singleton, and JsonApiExceptionHandler.

Result Extensions

Convert values directly to JSON:API responses:

// Success
var resource = new BookResource(book, includeCtx);
return resource.ToJsonApiResult("BOOK_FETCHED");
return resource.ToJsonApiResult("BOOK_CREATED", "Book created successfully", 201);

// Error from string
return "Book not found".ToJsonApiError("NOT_FOUND", 404);
return "Unauthorized".ToJsonApiError("UNAUTHORIZED", 401, "You must be logged in");

// Validation errors
return errors.ToJsonApiValidationError(validationDictionary);

Exception Handler

Unhandled exceptions are automatically mapped to JsonApiErrorDocument:

Exception HTTP Status Code
KeyNotFoundException 404 NOT_FOUND
ArgumentException 400 BAD_REQUEST
UnauthorizedAccessException 401 UNAUTHORIZED
NotSupportedException 405 METHOD_NOT_ALLOWED
Other 500 INTERNAL_ERROR

Stack traces are included when IncludeExceptionDetails = true (recommended for development only).

app.UseExceptionHandler(o => { });

Validation Filter

Add the endpoint filter to validate request bodies:

app.MapPost("/api/v1/books", (CreateBookRequest request) => { ... })
   .AddEndpointFilter<JsonApiValidationFilter>();

Validators implement IJsonApiValidator<T> and produce field-level errors with JSON:API source pointers:

{
  "responseCode": "VALIDATION_ERROR",
  "httpCode": 422,
  "errors": [
    {
      "code": "VALIDATION_ERROR",
      "detail": "Title is required",
      "status": "422",
      "source": { "pointer": "/data/attributes/title" }
    }
  ]
}

EF Core Integration

IQueryableEntity

Implement IQueryableEntity<T> on your entities to define filter, sort, and include logic as static methods:

public class Book : IQueryableEntity<Book>
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public int AuthorId { get; set; }
    public Author Author { get; set; } = null!;
    public DateTime CreatedAt { get; set; }

    public static IQueryable<Book> ApplyFilter(IQueryable<Book> query, object? filter)
    {
        if (filter is string search && !string.IsNullOrWhiteSpace(search))
            return query.Where(b => b.Title.Contains(search));
        return query;
    }

    public static IQueryable<Book> ApplySort(IQueryable<Book> query, string? sort) => sort switch
    {
        "title" => query.OrderBy(b => b.Title).ThenBy(b => b.Id),
        "-title" => query.OrderByDescending(b => b.Title).ThenByDescending(b => b.Id),
        _ => query.OrderByDescending(b => b.CreatedAt).ThenByDescending(b => b.Id)
    };

    public static IQueryable<Book> ApplyInclude(
        IQueryable<Book> query, object? include, IncludeContext context)
    {
        if (context.ShouldInclude("author"))
            query = query.Include(b => b.Author);
        return query;
    }
}

Paged Responses

ToPagedResponseAsync chains filter → sort → include → paginate → map in one call:

var includeCtx = IncludeParser.Parse(include);
var request = new ApiRequest<string?, string?, object>
{
    Include = include,
    Filter = filter,
    Sort = sort,
    Page = new RequestPage { Size = pageSize, Number = pageNumber }
};

var result = await db.Books
    .ToPagedResponseAsync<Book, string?, string?, object, BookResource>(
        request,
        (entity, ctx) => new BookResource(entity, ctx),
        includeCtx);

return result.ToJsonApiResult("BOOKS_FETCHED");

Supports both offset pagination (pageNumber/pageSize) and cursor pagination with PaginationCursor<TId>.

Source Generators

Reduce boilerplate with the [JsonApiResource] attribute:

[JsonApiResource("book", "/api/v1/books")]
public partial class BookResource
{
    public string Id { get; set; }

    [JsonApiAttribute]
    public string Title { get; set; }

    [JsonApiRelationship("author")]
    public AuthorResource? Author { get; set; }
}

The generator produces a partial class with the Type property and Links based on the resource type and base path.

Attributes are emitted via RegisterPostInitializationOutput, so consuming projects only need the analyzer reference — no additional package dependency.

JSON Output Examples

Success (relaxed mode):

{
  "responseCode": "BOOKS_FETCHED",
  "responseMessage": "Success",
  "httpCode": 200,
  "data": [
    {
      "id": "1",
      "type": "book",
      "attributes": {
        "title": "1984",
        "slug": "1984"
      },
      "relationships": {
        "author": {
          "id": "1",
          "type": "author",
          "attributes": {
            "name": "George Orwell",
            "slug": "george-orwell"
          }
        }
      }
    }
  ],
  "meta": {
    "paginationType": "cursor",
    "page": {
      "size": 10,
      "next": "eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTAxIiwiaWQiOjF9"
    }
  }
}

Error:

{
  "responseCode": "NOT_FOUND",
  "responseMessage": "Book not found",
  "httpCode": 404,
  "errors": [
    {
      "code": "NOT_FOUND",
      "detail": "Book not found",
      "status": "404"
    }
  ]
}

Sample Application

A complete working example is included at samples/JsonApiKit.Sample/. It demonstrates:

  • SQLite database with Author and Book entities
  • IQueryableEntity<T> implementations with filter, sort, and include
  • Resource constructors with IncludeContext depth tracking
  • ToPagedResponseAsync query pipeline
  • Exception handler and result extensions

Run it:

cd samples/JsonApiKit.Sample
dotnet run

Then visit:

  • GET /api/v1/books — list books with pagination
  • GET /api/v1/books?include=author — include author relationship
  • GET /api/v1/books/1 — single book
  • GET /api/v1/authors — list authors
  • GET /api/v1/error — exception handler demo

License

MIT — Copyright (c) 2026 JsonApiKit Contributors

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.0 162 4/1/2026
1.3.13 217 3/26/2026
1.3.11 104 3/26/2026
1.3.9 102 3/25/2026
1.3.8 102 3/25/2026
1.3.7 101 3/25/2026
1.3.6 118 3/7/2026
1.3.5 100 3/7/2026
1.3.4 102 3/7/2026
1.3.3 105 3/7/2026
1.3.2 104 3/5/2026
1.3.1 100 3/5/2026
1.3.0 110 3/5/2026
1.2.2 127 2/27/2026
1.2.1 109 2/26/2026
1.2.0 109 2/26/2026
1.1.0 112 2/26/2026
1.0.0 119 2/23/2026