JsonApiKit.AspNetCore 1.4.0

dotnet add package JsonApiKit.AspNetCore --version 1.4.0
                    
NuGet\Install-Package JsonApiKit.AspNetCore -Version 1.4.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.AspNetCore" Version="1.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JsonApiKit.AspNetCore" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="JsonApiKit.AspNetCore" />
                    
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.AspNetCore --version 1.4.0
                    
#r "nuget: JsonApiKit.AspNetCore, 1.4.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.AspNetCore@1.4.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.AspNetCore&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=JsonApiKit.AspNetCore&version=1.4.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, net10.0
JsonApiKit.AspNetCore ASP.NET Core integration — result extensions, exception handling, validation filters, DI registration. net8.0, net9.0, net10.0
JsonApiKit.EntityFrameworkCore EF Core integration — query pipeline, cursor/offset pagination, paged responses. net8.0, net9.0, net10.0
JsonApiKit.Dapper Dapper integration — SQL query pipeline, cursor/offset pagination, paged responses. net8.0, net9.0, net10.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

# Dapper query pipeline
dotnet add package JsonApiKit.Dapper

# 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>.

Dapper Integration

IDapperEntity

Implement IDapperEntity<T> on your entities to define filter, sort, and include logic using raw SQL:

using JsonApiKit.Dapper.Includes;

public class Book : IDapperEntity<Book>
{
    public int Id { get; set; }
    public string Title { get; set; } = "";
    public int AuthorId { get; set; }
    public Author? Author { get; set; }
    public ICollection<Category>? Categories { get; set; }
    public DateTime CreatedAt { get; set; }

    public static string BaseSql => "SELECT Id, Title, Slug, AuthorId, CreatedAt FROM Books";

    static IReadOnlyList<WhereClause> IDapperEntity<Book>.GetGlobalFilters() =>
        [new("DeletedAt IS NULL")];

    static IReadOnlyList<RelationshipDescriptor> IDapperEntity<Book>.GetRelationships() =>
    [
        new BelongsTo<Author>("author", nameof(AuthorId),
            "SELECT Id, Name FROM Authors WHERE Id IN @Ids"),
        new HasManyThrough<Category, (int BooksId, int CategoriesId)>("categories",
            "BooksId", "CategoriesId",
            "SELECT BooksId, CategoriesId FROM BookCategories WHERE BooksId IN @Ids",
            "SELECT Id, Name FROM Categories WHERE Id IN @Ids")
    ];

    public static IReadOnlyList<WhereClause> ApplyFilter(object? filter)
    {
        if (filter is string search && !string.IsNullOrWhiteSpace(search))
            return [new WhereClause("Title LIKE @search",
                new Dictionary<string, object?> { ["search"] = $"%{search}%" })];
        return [];
    }

    public static OrderByClause ApplySort(string? sort) => sort switch
    {
        "title" => new OrderByClause("Title ASC, Id ASC"),
        "-title" => new OrderByClause("Title DESC, Id DESC"),
        _ => new OrderByClause("CreatedAt DESC, Id DESC")
    };

    // No ApplyIncludeAsync needed — default implementation uses GetRelationships()
}

Key differences from EF Core's IQueryableEntity<T>:

  • BaseSql replaces the IQueryable<T> starting point
  • ApplyFilter returns WhereClause SQL fragments instead of chaining LINQ
  • ApplySort returns an OrderByClause SQL fragment instead of LINQ OrderBy
  • GetRelationships() declares includes declaratively — the engine handles SQL execution
  • ApplyIncludeAsync is optional — override it only for custom include logic

Global Filters

GetGlobalFilters() defines WHERE clauses applied to every query (e.g., soft-delete):

static IReadOnlyList<WhereClause> IDapperEntity<Book>.GetGlobalFilters() =>
    [new("deleted_at IS NULL")];

Bypass global filters for admin queries:

await connection.ToPagedResponseAsync<Book, ...>(..., ignoreGlobalFilters: true);

Relationship Descriptors

Declare relationships once — the engine handles SQL execution and entity mapping automatically.

Attribute-based (recommended) — co-locate relationship config with navigation properties and let the source generator emit GetRelationships() at compile time:

using JsonApiKit.Dapper.Includes;

public partial class Book : IDapperEntity<Book>
{
    public int AuthorId { get; set; }

    [BelongsTo(Sql = "SELECT Id, Name FROM Authors WHERE Id = ANY(@Ids)")]
    public Author? Author { get; set; }  // FK = "AuthorId" (convention), name = "author" (inferred)

    [HasManyThrough(
        PivotEntityKey = "BooksId", PivotRelatedKey = "CategoriesId",
        PivotSql = "SELECT BooksId, CategoriesId FROM BookCategories WHERE BooksId = ANY(@Ids)",
        RelatedSql = "SELECT Id, Name FROM Categories WHERE Id = ANY(@Ids)")]
    public ICollection<Category>? Categories { get; set; }  // name = "categories" (inferred)

    // No GetRelationships() method needed — source generator emits it at build time
}

The generator infers:

  • FK for [BelongsTo]: {PropertyName}Id by convention (override with [BelongsTo("CustomFkName")])
  • Name: lowercase property name (override with Name = "custom")
  • Related type: from property type (Author?) or collection element type (ICollection<Category>)

Three attribute types:

Attribute Relationship Required Properties
[BelongsTo] FK on this entity → single related Sql
[HasMany("FkName")] FK on related entity → collection Sql
[HasManyThrough] Junction table → many-to-many PivotEntityKey, PivotRelatedKey, PivotSql, RelatedSql

Compile-time diagnostics are reported for missing required properties or non-partial classes.

Manual approach — for advanced scenarios, you can still implement GetRelationships() directly:

public class Book : IDapperEntity<Book>
{
    static IReadOnlyList<RelationshipDescriptor> IDapperEntity<Book>.GetRelationships() =>
    [
        new BelongsTo<Author>("author", nameof(AuthorId),
            "SELECT ... FROM Authors WHERE Id = ANY(@Ids)"),

        new HasManyThrough<Category, (int BooksId, int CategoriesId)>("categories",
            "BooksId", "CategoriesId",
            "SELECT BooksId, CategoriesId FROM BookCategories WHERE BooksId = ANY(@Ids)",
            "SELECT ... FROM Categories WHERE Id = ANY(@Ids)")
    ];
}

Four descriptor records:

  • BelongsTo<T> — FK on this entity, loads single related (e.g. Book → Author)
  • HasMany<T> — FK on related entity, loads collection (e.g. Author → Books)
  • HasManyThrough<T, TPivot> — junction table with typed pivot struct
  • HasManyThroughDynamic<T> — junction table without pivot type (uses dynamic queries)

Custom ID converters for non-standard ID types (e.g. Ulid):

IdConverter.SetDefault(id => ((Ulid)id).ToByteArray());

Entities with custom include logic can still override ApplyIncludeAsync directly.

SQL Builder

The WhereClause and OrderByClause types represent SQL fragments:

// WHERE fragment with parameterized values (parameters are auto-remapped to prevent collisions)
new WhereClause("Title LIKE @search", new Dictionary<string, object?> { ["search"] = "%1984%" })

// ORDER BY fragment
new OrderByClause("CreatedAt DESC, Id DESC")

Dapper Paged Responses

ToPagedResponseAsync on DbConnection chains filter → sort → include → paginate → map, mirroring the EF Core pipeline:

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 connection
    .ToPagedResponseAsync<Book, string?, string?, object, BookResource>(
        request,
        (entity, ctx) => new BookResource(entity, ctx),
        includeCtx);

return result.ToJsonApiResult("BOOKS_FETCHED");

Three overloads are available, matching EF Core:

  1. Basic — no postProcess, no auto-cursor
  2. With postProcess callback — runs before resource mapping
  3. With ICursorableEntity<TId> — automatic keyset cursor pagination

Source Generators

The JsonApiKit.SourceGenerators package provides two Roslyn incremental generators that reduce boilerplate at compile time with zero reflection and full AOT compatibility.

Resource Generator

Reduce resource 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.

Dapper Relationship Generator

Declare Dapper relationship descriptors as attributes on navigation properties — the generator emits GetRelationships() at compile time:

public partial class Book : IDapperEntity<Book>
{
    public int AuthorId { get; set; }

    [BelongsTo(Sql = "SELECT Id, Name FROM Authors WHERE Id = ANY(@Ids)")]
    public Author? Author { get; set; }

    [HasMany("AuthorId", Sql = "SELECT Id, Title FROM Books WHERE AuthorId = ANY(@Ids)")]
    public ICollection<Book>? Books { get; set; }

    [HasManyThrough(
        PivotEntityKey = "BooksId", PivotRelatedKey = "CategoriesId",
        PivotSql = "SELECT BooksId, CategoriesId FROM BookCategories WHERE BooksId = ANY(@Ids)",
        RelatedSql = "SELECT Id, Name FROM Categories WHERE Id = ANY(@Ids)")]
    public ICollection<Category>? Categories { get; set; }
}
// Generated at compile time:
// partial class Book {
//     static IReadOnlyList<RelationshipDescriptor> IDapperEntity<Book>.GetRelationships() => [ ... ];
// }

The generator reports compile-time diagnostics:

Code Description
JAPI001 [BelongsTo] requires Sql property
JAPI002 [HasMany] requires Sql property
JAPI003 [HasManyThrough] requires PivotSql and RelatedSql
JAPI004 [HasManyThrough] requires PivotEntityKey and PivotRelatedKey
JAPI006 Class with relationship attributes must be partial

All 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 Applications

EF Core Sample

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
cd samples/JsonApiKit.Sample
dotnet run

Dapper Sample

A Dapper equivalent is at samples/JsonApiKit.Dapper.Sample/. It demonstrates:

  • SQLite database with raw SQL table creation
  • IDapperEntity<T> implementations with SQL-based filter, sort, and include
  • Post-fetch relationship loading via ApplyIncludeAsync
  • ToPagedResponseAsync on DbConnection
cd samples/JsonApiKit.Dapper.Sample
dotnet run

Both samples expose the same endpoints:

  • 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 152 4/1/2026
1.3.13 206 3/26/2026
1.3.11 102 3/26/2026
1.3.9 97 3/25/2026
1.3.8 107 3/25/2026
1.3.7 109 3/25/2026
1.3.6 114 3/7/2026
1.3.5 98 3/7/2026
1.3.4 104 3/7/2026
1.3.3 106 3/7/2026
1.3.2 103 3/5/2026
1.3.1 103 3/5/2026
1.3.0 113 3/5/2026
1.2.2 125 2/27/2026
1.2.1 104 2/26/2026
1.2.0 109 2/26/2026
1.1.0 107 2/26/2026
1.0.0 110 2/23/2026