Kanject.Core.Api.Annotations
2.0.2
Prefix Reserved
dotnet add package Kanject.Core.Api.Annotations --version 2.0.2
NuGet\Install-Package Kanject.Core.Api.Annotations -Version 2.0.2
<PackageReference Include="Kanject.Core.Api.Annotations" Version="2.0.2" />
<PackageVersion Include="Kanject.Core.Api.Annotations" Version="2.0.2" />
<PackageReference Include="Kanject.Core.Api.Annotations" />
paket add Kanject.Core.Api.Annotations --version 2.0.2
#r "nuget: Kanject.Core.Api.Annotations, 2.0.2"
#:package Kanject.Core.Api.Annotations@2.0.2
#addin nuget:?package=Kanject.Core.Api.Annotations&version=2.0.2
#tool nuget:?package=Kanject.Core.Api.Annotations&version=2.0.2
Kanject.Core.Api.Annotations
Kanject.Core.Api.Annotations is a Roslyn source generator plus analyzers for ASP.NET Core minimal APIs. You write endpoints as public static methods on a static class marked [ApiEndpoints]; at compile time it generates the MapGroup / MapGet / MapPost … registration code, carries your XML doc comments into OpenAPI summaries and descriptions, and writes a Markdown catalog of every route on each build.
Analyzers catch the mistakes that would otherwise make the generator skip an endpoint without telling you — a non-static class, a private method, duplicate routes — and four of them come with code fixes.
Installation
dotnet add package Kanject.Core.Api.Abstractions
dotnet add package Kanject.Core.Api.Annotations
<ItemGroup>
<PackageReference Include="Kanject.Core.Api.Abstractions" />
<PackageReference Include="Kanject.Core.Api.Annotations" PrivateAssets="all" />
</ItemGroup>
This package is not marked as a development dependency, so dotnet add package writes a plain reference without PrivateAssets or IncludeAssets. Add PrivateAssets="all" yourself so the generator and its build targets stay in your project instead of flowing to projects that reference it.
The attribute types live in Kanject.Core.Api.Abstractions (there is no separate .Attributes package). Kanject.Core.Api and Kanject.Core.ApiV2 both bring that package in, but neither references this generator — add it explicitly wherever you declare [ApiEndpoints] classes. It depends on Kanject.Core.Annotations, which NuGet restores automatically.
The analyzer assembly targets netstandard2.0. Use it in ASP.NET Core projects on .NET 8, .NET 9 or .NET 10: the attribute types come from Kanject.Core.Api.Abstractions (net8.0–net10.0), and the generated code uses minimal-API route groups.
Quick start
using Kanject.Core.Api.Abstractions.Attributes;
using 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(IOrderStore store, CancellationToken ct)
=> Results.Ok(await store.ListAsync(ct));
/// <summary>Returns one order.</summary>
/// <remarks>Responds 404 when the order does not exist.</remarks>
[HttpGet("{id:guid}")]
public static async Task<IResult> GetOrder(Guid id, IOrderStore store, CancellationToken ct)
=> await store.FindAsync(id, ct) is { } order ? Results.Ok(order) : Results.NotFound();
/// <summary>Liveness probe for the orders group.</summary>
[AllowAnonymous]
[HttpGet("ping")]
public static IResult PingOrders() => Results.Ok();
}
using Contoso.Orders.Endpoints;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(/* your scheme */);
builder.Services.AddAuthorization();
builder.Services.AddScoped<IOrderStore, OrderStore>();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapEndpoints(); // every [ApiEndpoints] class in this project
app.Run();
Enable XML documentation in the project so the compiler parses your doc comments — the generator reads <summary> and <remarks> from them, and without it KANAPI011 reports every endpoint as undocumented:
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
What gets generated
For each [ApiEndpoints] class, in the class's namespace — the global namespace for a class declared without one, such as beside the top-level statements of a minimal-API Program.cs (abridged):
public sealed class OrderEndpointsEndpointOptions
{
public Action<RouteHandlerBuilder>? ListOrders { get; set; }
public Action<RouteHandlerBuilder>? GetOrder { get; set; }
public Action<RouteHandlerBuilder>? PingOrders { get; set; }
}
public static class OrderEndpointsRouteRegistration
{
public static IEndpointRouteBuilder MapOrderEndpoints(this IEndpointRouteBuilder app,
Action<RouteGroupBuilder>? configureGroup = null,
Action<OrderEndpointsEndpointOptions>? configureEndpoints = null)
{
var group = app.MapGroup("api/orders")
.WithTags("Orders")
.RequireAuthorization()
.WithDescription("Order management endpoints.");
configureGroup?.Invoke(group);
// ...
var endpointGetOrder = group.MapGet("{id:guid}", global::Contoso.Orders.Endpoints.OrderEndpoints.GetOrder)
.WithName(nameof(global::Contoso.Orders.Endpoints.OrderEndpoints.GetOrder))
.WithSummary("Returns one order.")
.WithDescription("Responds 404 when the order does not exist.");
endpointOptions.GetOrder?.Invoke(endpointGetOrder);
// ...
return app;
}
}
A class nested in another type is generated beside it at the top level of the namespace, named after its containing types and itself: TodoFeature.Endpoints generates TodoFeatureEndpointsRouteRegistration.MapTodoFeatureEndpoints() and TodoFeatureEndpointsEndpointOptions, so feature classes that each nest an Endpoints class don't collide.
Plus one KanjectEndpointRegistration.MapEndpoints() that calls every group's Map{ClassName}(). It is generated into the shortest namespace among your endpoint classes, so import that namespace where you call it. If any endpoint class is in the global namespace, so is MapEndpoints(), and it needs no using at all.
That is the output when generated request delegates are off. When they are on — under PublishAot or PublishTrimmed, see Native AOT — the group.MapGet(...) line becomes OrderEndpointsRequestDelegates.MapGetOrder(group, "{id:guid}"), a generated method in the same file; the rest of the chain is identical.
Groups, routes and authorization
[ApiEndpoints] takes the OpenAPI tag and the route prefix positionally, or as named properties:
| Property | Default | Effect |
|---|---|---|
EndpointGroupName |
The class name (for a nested class, its generated name: TodoFeatureEndpoints) |
WithTags(...) on the group |
Route |
"" |
Prefix passed to MapGroup(...) |
RequireAuthorization |
true |
RequireAuthorization() on the group — register authentication and authorization, or set this to false |
AuthPolicy |
none | Group-wide policy, RequireAuthorization("<policy>") — applied only while RequireAuthorization is true |
Verb attributes take an optional route template relative to the group ([HttpGet("{id:guid}")]); no template maps the group root. Per method, use ASP.NET Core's own attributes from Microsoft.AspNetCore.Authorization: [AllowAnonymous] opts the endpoint out of the group requirement, and [Authorize(Policy = "...")] or [Authorize(Roles = "...")] adds a requirement. The generator mirrors them as AllowAnonymous() / RequireAuthorization(...) calls on the endpoint.
Configuring groups and endpoints
Call the per-group method instead of MapEndpoints() when a group needs extra conventions:
app.MapOrderEndpoints(
configureGroup: group => group.RequireCors("frontend"),
configureEndpoints: endpoints =>
{
endpoints.PingOrders = endpoint => endpoint.ExcludeFromDescription();
});
MapEndpoints() maps every group with no callbacks. Don't call it and a per-group Map…() for the same class, or the routes are registered twice.
Rules and gotchas
- The class must be
static(KANAPI001) and endpoint methodspublic static(KANAPI002); anything else is skipped by the generator. - The generated code calls your endpoints from the top level of the class's namespace, so the class and every type containing it must be
publicorinternal(orprotected internal), notprivate,protected,private protectedorfile(KANAPI006), and none of them generic (KANAPI016). Such a class is reported and none of its endpoints are mapped. - Two classes that generate the same names in one namespace —
TodoFeature.Endpointsbeside a top-levelTodoFeatureEndpoints— are reported (KANAPI017), and neither is mapped until one is renamed. Classes of the same name in different namespaces are fine:MapEndpoints()calls each by its full name. Call them yourself through the registration class (Contoso.Admin.UsersRouteRegistration.MapUsers(app)) if both namespaces are imported where you do. - Each endpoint is named after its method (
WithName(nameof(...))). ASP.NET Core requires endpoint names to be unique, and OpenAPI tools use them as operation IDs, so keep method names unique across all[ApiEndpoints]classes —ListOrders, notList. - Only the first verb attribute on a method is used (KANAPI013).
- Return
IResultor a concrete type; MVC'sIActionResultbelongs to controllers (KANAPI007), andvoid/Taskgive an implicit empty 200 (KANAPI008). - The Kanject verb attributes share their names with
Microsoft.AspNetCore.Mvc.HttpGetAttributeand friends. If an endpoint file also importsMicrosoft.AspNetCore.Mvc(for[FromBody],[FromServices], …),[HttpGet]becomes ambiguous — alias or fully qualify one of them.
Native AOT: generated request delegates
group.MapGet(route, Endpoints.Method) builds the route with ASP.NET Core's reflection-based RequestDelegateFactory, which is [RequiresUnreferencedCode] and [RequiresDynamicCode]. In your own code ASP.NET Core's Request Delegate Generator replaces those calls when you publish with Native AOT, but it is a source generator and cannot see calls another generator emits. So this generator writes the request delegates itself.
| MSBuild property | Effect |
|---|---|
KanjectApiGeneratedRequestDelegates |
true emits generated request delegates; false keeps RequestDelegateFactory whatever the publish mode |
EnableRequestDelegateGenerator |
When the property above is unset, generated delegates follow this ASP.NET Core switch, which the SDK turns on for PublishAot and PublishTrimmed |
With them on, each route is registered through RouteHandlerServices.Map — the hook the Request Delegate Generator uses — with typed code for:
- route, query and header values, including arrays,
StringValuesand everyTryParsekindRequestDelegateFactorysupports (enums, dates, numbers,Guid,Uri, your ownTryParse/IParsable<T>types) - JSON bodies (
[FromBody]or inferred), read with theJsonTypeInfofrom yourHttpJsonOptionsresolver chain [FromServices],[FromKeyedServices]and inferred services;HttpContext,HttpRequest,HttpResponse,ClaimsPrincipal,CancellationToken,Stream,PipeReader[AsParameters],BindAsync/IBindableFromHttpContext<T>,IFormFile,IFormFileCollection,IFormCollectionand simple[FromForm]values- every return shape (
IResult, typed results, JSON,string,object,void,Task/ValueTaskof each) - endpoint filters, including group filters, which see the bound arguments as typed values
The generated code reproduces what RequestDelegateFactory does for the same method on the ASP.NET Core version you compile against: the binding order, 400 and 415 responses, RouteHandlerOptions.ThrowOnBadRequest, the debug log events (under the Microsoft.AspNetCore.Http.RequestDelegateFactory category), startup errors, and the endpoint metadata it infers (accepts/produces, antiforgery, parameter binding and cookie-redirect metadata). The group conventions, WithName/WithSummary/WithTags, configureEndpoints callbacks, RequireAuthorization and DisableAntiforgery are untouched.
What a Native AOT app has to provide is what any Native AOT minimal API needs — JSON metadata for its request and response types:
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
internal partial class AppJsonContext : JsonSerializerContext;
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
Set KanjectApiGeneratedRequestDelegates to true in the project (not only at publish) to run the same binding in development and tests that you ship.
A route whose shape the generator does not reproduce exactly — a generic method, a ref parameter, a custom binding-source attribute, a complex [FromForm] type, a nested [AsParameters], a malformed TryParse/BindAsync, a custom awaitable return, and the like — keeps MapGet/MapPost and RequestDelegateFactory, and the build reports KANAPI014 on it with the reason. Change the shape, or accept the reflection-based route and suppress KANAPI014 on that method.
API schema document
Each time a build compiles the project, the package's MSBuild targets write <AssemblyName>.apischema.md next to the project file: a Markdown catalog with an overview, endpoint groups, a route map, an authorization matrix, OpenAPI documentation coverage and registration snippets. It is useful as reference documentation and as context for AI coding assistants. (Versions up to 2.0.0 described this file but wrote it only for a project that set EmitCompilerGeneratedFiles itself.)
| MSBuild property | Default | Effect |
|---|---|---|
GenerateApiSchema |
true |
Set false to skip writing the file |
ApiSchemaOutputDir |
Project directory | Relative (to the project) or absolute output directory |
The document is read from the generator's source as Roslyn writes it to disk, so the package's props set EmitCompilerGeneratedFiles to true unless you have set it yourself. Generated sources then also land under obj/<configuration>/<framework>/generated/, or under your CompilerGeneratedFilesOutputPath. If you set EmitCompilerGeneratedFiles to false, no document is written and the build says why.
GenerateApiSchema=false in Directory.Build.props turns both off, because it is read before the package's defaults. In the project file it is read after them: the document is skipped, but generated sources are still written unless you also set EmitCompilerGeneratedFiles to false there.
The document is rewritten only when the compiler runs. An up-to-date incremental build leaves it as it is, so rebuild to recreate a deleted one. A compile that produces no schema (no [ApiEndpoints] classes left, or KANJECTAPISCHEMA001) writes nothing and leaves an earlier document in place. The generator also embeds the same Markdown in your compiled assembly as [assembly: AssemblyMetadata("ApiSchema", ...)]; GenerateApiSchema=false does not remove that attribute.
Diagnostics
| ID | Severity | What it means |
|---|---|---|
| KANAPI001 | Error | [ApiEndpoints] class is not static, so the generator skips it (code fix: make static). |
| KANAPI002 | Error | A method with a verb attribute is not public static, so it is skipped (code fix: make public static). |
| KANAPI003 | Warning | A Kanject verb attribute is on a method whose class lacks [ApiEndpoints] and has no effect (code fix: add [ApiEndpoints]). |
| KANAPI004 | Warning | [ApiEndpoints] class contains no verb-annotated methods. |
| KANAPI005 | Error | Two methods in a group map the same verb and route template — a runtime route conflict. |
| KANAPI006 | Error | The [ApiEndpoints] class, or a type containing it, is private, protected, private protected or file, so generated code cannot call it and none of its endpoints are mapped. The message names the type. |
| KANAPI007 | Info | Endpoint returns MVC IActionResult / ActionResult; return IResult or a concrete type. |
| KANAPI008 | Warning | Endpoint returns void or bare Task; return an IResult for an explicit status code. |
| KANAPI009 | Info | [AllowAnonymous] is redundant because the group sets RequireAuthorization = false. |
| KANAPI010 | Info | [Authorize] without a policy or roles is redundant because the group already requires authorization. |
| KANAPI011 | Info | Endpoint method has no XML <summary>, so its OpenAPI summary will be empty. |
| KANAPI012 | Warning | Route template starts with /, which can produce // after the group prefix (code fix: remove it). |
| KANAPI013 | Warning | Method has more than one verb attribute; only the first is used. |
| KANAPI014 | Warning | Generated request delegates are on, but this endpoint's shape is not one the generator reproduces exactly, so it stays on RequestDelegateFactory (reflection; IL2026/IL3050 under Native AOT). The message says why. |
| KANAPI015 | Warning | Generated request delegates were requested, but the project does not reference ASP.NET Core 8.0 or later. |
| KANAPI016 | Error | The [ApiEndpoints] class is generic or nested in a generic type; a generated registration has no type arguments to supply, so none of its endpoints are mapped. |
| KANAPI017 | Error | Two [ApiEndpoints] classes generate the same {Name}RouteRegistration and Map{Name} in one namespace (a nested class's name includes its containing types'), so neither is mapped; rename one. |
| KANJECTAPISCHEMA001 | Warning | Generating the API schema document failed; route registrations are still generated. |
Related packages
| Package | Role | Availability |
|---|---|---|
Kanject.Core.Api.Abstractions |
The [ApiEndpoints] and verb attribute types, plus response envelope helpers for minimal APIs |
nuget.org |
Kanject.Core.Api |
Controllers, exception middleware (Kanject envelope errors) and tenant middleware | nuget.org |
Kanject.Core.ApiV2 |
Controllers, exception middleware (Problem Details errors) and tenant middleware | nuget.org |
Kanject.Core.Annotations |
Shared Kanject source-generator infrastructure (dependency of this package) | nuget.org |
License
Licensed under the Kanject Code Libraries License Agreement (KCLLA); the full text ships in this package as LICENSE.md. Organizations whose trailing-twelve-month gross revenue and total funding raised are each below US$250,000 may use it at no cost under the Free Tier. At or above either threshold a commercial license is required — contact commercial@kanjectbusiness.com.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- Kanject.Core.Annotations (>= 3.15.0)
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 |
|---|---|---|
| 2.0.2 | 91 | 10/22/2026 |
| 2.0.1 | 191 | 10/14/2026 |
| 2.0.0 | 81 | 10/13/2026 |
| 1.9.0 | 92 | 10/12/2026 |
| 1.8.0 | 121 | 10/5/2026 |
| 1.7.1 | 100 | 10/5/2026 |
| 1.7.0 | 100 | 10/2/2026 |
| 1.6.1 | 122 | 9/27/2026 |
| 1.6.0 | 104 | 9/27/2026 |
| 1.5.7 | 105 | 9/26/2026 |
| 1.5.6 | 142 | 9/7/2026 |
| 1.5.5 | 121 | 8/27/2026 |
| 1.5.4 | 120 | 8/22/2026 |
| 1.5.3 | 126 | 8/10/2026 |
| 1.5.2 | 120 | 8/9/2026 |
| 1.5.1 | 137 | 8/5/2026 |
| 1.5.0 | 127 | 8/5/2026 |
| 1.4.0 | 133 | 8/3/2026 |
| 1.3.5 | 133 | 7/30/2026 |
| 1.3.4 | 133 | 7/18/2026 |