Kanject.Core.Api

View .md

ASP.NET Core building blocks that give every Kanject HTTP service the same baseline: a consistent Response<T> envelope, exception-to-error middleware, and x-tenant-id tenant resolution. Write endpoints as controllers on BaseController, or as minimal-API endpoint classes whose routes are source-generated. Free under the Community licence.

Install

bash
dotnet add package Kanject.Core.Api

Register

csharp
using Kanject.Core.Api.Extensions;​var builder = WebApplication.CreateBuilder(args);​builder.Services.AddControllers();builder.Services.AddCoreExceptionHandlerMiddleware();   // consistent error envelopebuilder.Services.AddTenantMiddleware();                 // x-tenant-id resolution​var app = builder.Build();​app.UseRouting();app.UseCoreExceptionHandlerMiddleware();app.UseTenantMiddleware();app.MapControllers();app.Run();

Two middlewares, registered as services and then used in the pipeline: CoreExceptionHandler turns uncaught exceptions into a consistent JSON error envelope, and TenantMiddleware resolves the caller's tenant from the x-tenant-id header into the request context.

Controllers

Derive from BaseController and return through ApiResponse(...), which wraps your payload (and optional PayloadMetadata for pagination) in the shared Response<T> envelope. Errors come back in the same Response<string> shape, so every endpoint — success or failure — has one predictable body:

csharp
using Kanject.Core.Api.Abstractions.Models;   // ApiResponseDataset<T>, PayloadMetadatausing Kanject.Core.Api.Controller;using Microsoft.AspNetCore.Mvc;​[ApiController][Route("api/orders")]public sealed class OrdersController(OrderService orders) : BaseController{    // BaseController writes Kanject.Core.Api.Models.Response<T>. The Abstractions    // package has a Response<T> of its own, so name the one you document in full.    [HttpGet]    [ProducesResponseType(typeof(Kanject.Core.Api.Models.Response<ApiResponseDataset<OrderDto>>), StatusCodes.Status200OK)]    [ProducesResponseType(typeof(Kanject.Core.Api.Models.Response<string>), StatusCodes.Status400BadRequest)]    public async Task<IActionResult> ListAsync(        [FromQuery] int pageSize = 20,        [FromQuery] string? pageToken = null,        CancellationToken cancellationToken = default)    {        // ListAsync returns (data, metadata): one page and its PayloadMetadata.        var (items, metadata) = await orders.ListAsync(pageSize, pageToken, cancellationToken);​        // BaseController.ApiResponse wraps the payload in Response<T>.        return ApiResponse(items, metadata, "Orders retrieved.");    }}

Kanject's NoSQL reads (the generated Find…Async finders, GetAllAsync and SqlQueryAsync) return (payload, metadata): the page and a PayloadMetadata that already holds the page size, item count and continuation token. A service that wraps one, like ListAsync here, passes the pair straight to ApiResponse. When a list gives you items and a token instead, build the metadata yourself with new PayloadMetadata(pageSize, items.Count, pageToken: nextToken). A non-empty token sets HasNextPage.

Minimal-API endpoints

The second option is a minimal API without the registration boilerplate. Put public static methods on a static class marked [ApiEndpoints], and the Kanject.Core.Api.Annotations source generator writes the MapGroup / MapGet / MapPost registration at compile time. Each method's XML <summary> and <remarks> become its OpenAPI summary and description. Kanject.Core.Api already brings in the attributes, but not the generator, so add it to the project that declares the classes:

bash
dotnet add package Kanject.Core.Api.Annotations

dotnet add package writes a plain reference. Add PrivateAssets="all" to it, so the generator stays in this project instead of flowing to the projects that reference it, and turn on XML documentation:

xml
<PropertyGroup>  <!-- the generator reads <summary> / <remarks> from your XML doc comments -->  <GenerateDocumentationFile>true</GenerateDocumentationFile></PropertyGroup>
csharp
using Kanject.Core.Api.Abstractions.Attributes;   // [ApiEndpoints], [HttpGet], ...using Kanject.Core.Api.Abstractions.Endpoints;    // ApiResponse(...) for minimal APIsusing Microsoft.AspNetCore.Authorization;​namespace Contoso.Orders.Endpoints;​/// <summary>Order management endpoints.</summary>[ApiEndpoints("Orders", "api/orders")]public static class OrderEndpoints{    /// <summary>Lists the caller's orders.</summary>    [HttpGet]    public static async Task<IResult> ListOrders(        OrderService orders, int pageSize = 20, string? pageToken = null,        CancellationToken cancellationToken = default)    {        var (items, metadata) = await orders.ListAsync(pageSize, pageToken, cancellationToken);​        // The same Response<T> envelope BaseController.ApiResponse writes.        return items.ApiResponse(metadata, "Orders retrieved.");    }​    /// <summary>Liveness probe for the orders group.</summary>    [AllowAnonymous]    [HttpGet("ping")]    public static IResult PingOrders() => Results.Ok();}

Then map every endpoint class with one call. The exception and tenant middleware are ordinary pipeline middleware, so they serve endpoint classes as they do controllers:

csharp
using Contoso.Orders.Endpoints;using Kanject.Core.Api.Extensions;​var builder = WebApplication.CreateBuilder(args);​builder.Services.AddAuthentication(/* your scheme */);builder.Services.AddAuthorization();builder.Services.AddCoreExceptionHandlerMiddleware();builder.Services.AddTenantMiddleware();builder.Services.AddScoped<OrderService>();​var app = builder.Build();​app.UseRouting();app.UseCoreExceptionHandlerMiddleware();app.UseAuthentication();app.UseAuthorization();app.UseTenantMiddleware();              // after authentication​app.MapEndpoints();   // every [ApiEndpoints] class in the projectapp.Run();

[ApiEndpoints] takes the group name and route positionally, or as named properties:

  • EndpointGroupName — the OpenAPI tag (WithTags). Defaults to the class name.
  • Route — the prefix passed to MapGroup. Defaults to the root. A verb attribute's template ([HttpGet("{id:guid}")]) is relative to it.
  • RequireAuthorization — true by default, so the group needs authentication and authorization registered. Set it to false for a public group.
  • AuthPolicy — a named policy for the whole group, applied while RequireAuthorization is true.
  • Per method, ASP.NET Core's own [AllowAnonymous] opts out of the group requirement, and [Authorize(Policy = "…")] or [Authorize(Roles = "…")] adds one.

Configure a group or one endpoint

For every class the generator also writes Map{ClassName}() and a {ClassName}EndpointOptions type with one property per endpoint method. Call that method to add conventions to the whole group (configureGroup) or to a single endpoint (configureEndpoints):

csharp
// Instead of MapEndpoints() for this group — never both, or its routes register twice.app.MapOrderEndpoints(    configureGroup: group => group.RequireCors("frontend"),    configureEndpoints: endpoints =>    {        endpoints.PingOrders = endpoint => endpoint.ExcludeFromDescription();    });
  • The class must be static (KANAPI001) and its endpoint methods public static (KANAPI002). The generator skips anything else, and the analyzers say so at build time.
  • Each endpoint is named after its method and OpenAPI tools use that name as the operation ID, so keep method names unique across every endpoint class: ListOrders, not List.
  • Return IResult or a concrete type. IActionResult belongs to controllers (KANAPI007).
  • The Kanject [HttpGet] / [HttpPost] attributes share their names with MVC's. In a file that also imports Microsoft.AspNetCore.Mvc, alias or fully qualify one of them.
  • Every build writes <AssemblyName>.apischema.md beside the project: a catalogue of the routes, the authorization matrix and OpenAPI coverage. Set GenerateApiSchema to false to skip it.

What ships with it

  • BaseController + ApiResponse(...) → a consistent Response<T> / ApiResponseDataset<T> envelope, with PayloadMetadata for pagination
  • [ApiEndpoints] and the [HttpGet] … [HttpPatch] verb attributes (Kanject.Core.Api.Abstractions), plus ApiResponse(...) helpers that write the same envelope from a minimal API
  • Kanject.Core.Api.Annotations — the source generator and analyzers that turn endpoint classes into route registrations, a per-group Map{ClassName}() and an .apischema.md route catalogue
  • AddCoreExceptionHandlerMiddleware / UseCoreExceptionHandlerMiddleware — uncaught exceptions become a uniform JSON error response
  • AddTenantMiddleware / UseTenantMiddleware — x-tenant-id resolved into the request-scoped tenant context
  • Distributed as a public NuGet Community package (free under the $250K threshold)
Was this page helpful?