Kanject.Core.Adapter.Annotations 1.9.0

Prefix Reserved
dotnet add package Kanject.Core.Adapter.Annotations --version 1.9.0
                    
NuGet\Install-Package Kanject.Core.Adapter.Annotations -Version 1.9.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="Kanject.Core.Adapter.Annotations" Version="1.9.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Kanject.Core.Adapter.Annotations" Version="1.9.0" />
                    
Directory.Packages.props
<PackageReference Include="Kanject.Core.Adapter.Annotations" />
                    
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 Kanject.Core.Adapter.Annotations --version 1.9.0
                    
#r "nuget: Kanject.Core.Adapter.Annotations, 1.9.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 Kanject.Core.Adapter.Annotations@1.9.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=Kanject.Core.Adapter.Annotations&version=1.9.0
                    
Install as a Cake Addin
#tool nuget:?package=Kanject.Core.Adapter.Annotations&version=1.9.0
                    
Install as a Cake Tool

Kanject.Core.Adapter.Annotations

A Roslyn incremental source generator that eliminates boilerplate from AbstractServiceAdapter subclasses. Annotate your adapter methods with [AdapterEndpoint], declare authentication once at the class level, and the generator produces fully implemented, virtual endpoint methods — ready to override when you need custom behaviour. It also generates DI registration helpers and Markdown / OpenAPI documentation for every adapter, and ships analyzers with code fixes for common mistakes.

Installation

You normally do not reference this package yourself. Kanject.Core.Adapter depends on it, and the generator, analyzers, code fixes, and build targets reach any project that references the runtime:

dotnet add package Kanject.Core.Adapter

Reference it directly only to pin the generator version explicitly:

<ItemGroup>
  <PackageReference Include="Kanject.Core.Adapter" />
  <PackageReference Include="Kanject.Core.Adapter.Annotations" PrivateAssets="all" />
</ItemGroup>

Things worth knowing about how this package is shaped:

  • There is no separate attributes package. [AdapterEndpoint], [AdapterAuth] and the other attributes, plus the HttpVerb, AuthScheme, ErrorBehavior and RequestPayloadContentType enums, live in Kanject.Core.Adapter (namespaces Kanject.Core.Adapter.Attributes and Kanject.Core.Adapter.Enums). Generated code also calls Kanject.Core.Adapter and Kanject.Core.Api.Abstractions, so this package is not useful without the runtime.
  • It is not marked as a development dependency, so dotnet add package writes a plain reference with no IncludeAssets line. The package ships the generator under analyzers/dotnet/cs and an MSBuild targets file (schema/OpenAPI extraction) under both build/ and buildTransitive/, which is how the targets reach projects that get this package through Kanject.Core.Adapter.
  • The generator targets netstandard2.0 and runs inside the compiler; the code it emits requires Kanject.Core.Adapter, which targets .NET 8, .NET 9 and .NET 10.

Quick start

using Kanject.Core.Adapter;
using Kanject.Core.Adapter.Attributes;
using Kanject.Core.Adapter.Enums;

[AdapterAuth(AuthScheme.Bearer, SettingsType = typeof(MyApiSettings), TokenProperty = nameof(MyApiSettings.ApiToken))]
[AdapterBaseUrl(nameof(MyApiSettings.ServiceUrl))]
[AdapterDisablePing]
public partial class MyServiceAdapter(HttpClient httpClient, MyApiSettings settings)
    : AbstractServiceAdapter(httpClient, shouldDisposeHttpClient: false)
{
    private readonly MyApiSettings _settings = settings;

    /// <summary>Creates an order.</summary>
    [AdapterEndpoint(HttpVerb.Post, "api/v1/orders")]
    public virtual partial Task<OrderResponse?> CreateOrderAsync(
        CreateOrderRequest request, CancellationToken cancellationToken);

    /// <summary>Gets an order by ID.</summary>
    [AdapterEndpoint(HttpVerb.Get, "api/v1/orders/{OrderId}")]
    public virtual partial Task<OrderResponse?> GetOrderAsync(
        GetOrderRequest request, CancellationToken cancellationToken);
}

The generator produces an endpoint file such as MyAssembly.MyNamespace.MyServiceAdapter.Endpoints.g.cs containing the full method bodies — URL resolution, auth headers, HTTP call, error handling, and response deserialization — all as virtual methods you can override.

Register the settings instance and the generated adapters from your composition root. The adapter's constructor asks for MyApiSettings itself, so register that type (not only IOptions<MyApiSettings>):

using Microsoft.Extensions.Options;

services
    .AddOptions<MyApiSettings>()
    .Bind(configuration.GetSection("MyApi"));
services.AddSingleton(sp => sp.GetRequiredService<IOptions<MyApiSettings>>().Value);

services.AddGeneratedServiceAdapters();

Attributes

[AdapterEndpoint] — Method-level

Marks a partial method on a partial adapter class for code generation.

Parameter Type Required Default Description
verb HttpVerb Yes — HTTP method: Get, Post, Put, Delete, Patch
path string Yes — Relative URL path. Supports {RouteParam} placeholders
ContentType RequestPayloadContentType No Json Json or Xml
Auth AuthScheme? No null (inherit) Per-endpoint override. null inherits the class-level scheme
ErrorBehavior ErrorBehavior No Throw Throw, StatusSwitch, or Custom
ErrorMessage string? No null Message for the ApiServiceException thrown by ErrorBehavior.Throw (default "{MethodName} failed")
BaseUrlProperty string? No null Settings property providing an alternative base URL
AdditionalHeaders string[]? No null Extra headers as "headerName:SettingsPropertyName" pairs
ResponseWrapper Type? No null Wrapper type to deserialize before unwrapping
UnwrapProperty string? No null Property on the wrapper to unwrap (e.g. "Data")
NotFoundReturnsNull bool No false Return null for HTTP 404/403. Requires Task<T?>; analyzer KAD0003 offers a nullable-return fix.
Query string? No null Query-string template appended to path. Tokens resolve against method parameters, {parameter.Property} on any object parameter, then first request DTO properties; values are URL-encoded.

[AdapterHeader] — Method-level (AllowMultiple)

Adds a per-request header to one generated endpoint without mutating HttpClient.DefaultRequestHeaders.

[AdapterEndpoint(HttpVerb.Get, "tracking/v1/events", Query = "barcode={barcode}&descriptionType=CLIENT")]
[AdapterHeader("apikey", "{settings.TrackingApiKey}")]
public virtual partial Task<TrackingResponse?> GetTrackingAsync(string barcode, CancellationToken ct);

Header value templates support {settings.Property}, {request.Property}, and bare {parameterName} tokens. Unresolved tokens are reported as KAD0005, with a code fix when the analyzer can find a close match.

[AdapterAuth] — Class-level

Declares the default authentication scheme for all endpoints on the class.

Parameter Type Required Default Description
scheme AuthScheme Yes — None, Bearer, Basic, ApiKey, OAuth, Identity
SettingsType Type? No null Type of the settings object holding credentials
UsernameProperty string? No null Settings property for the username (Basic auth)
PasswordProperty string? No null Settings property for the password (Basic auth)
TokenProperty string? No null Settings property for the bearer token
HeaderMappings string[]? No null API key headers as "headerName:SettingsPropertyName" pairs

[OAuthClientCredentials] — Class-level

Generates a thread-safe OAuth2 client-credentials token lifecycle (GetOrRefreshAccessTokenAsync). The token request is a form-encoded POST with grant_type=client_credentials, client_id, client_secret, and — when set — audience and scope.

Parameter Type Required Default Description
SettingsType Type? No null Type of the settings object
TokenUrlProperty string Yes — Settings property for the token endpoint URL
ClientIdProperty string Yes — Settings property for the client ID
ClientSecretProperty string Yes — Settings property for the client secret
Audience string? No null Settings property for the audience parameter
BufferSeconds int No 60 Seconds before expiry to refresh proactively
Scopes string[]? No null OAuth scopes to request (sent space-separated)
AccessTokenProperty string No "AccessToken" Token-response field, written in PascalCase and read as snake_case (access_token)
ExpiresInProperty string No "ExpiresIn" Expires-in field, written in PascalCase and read as snake_case (expires_in)
CacheKey string? No null Enables cross-instance token caching through ICacheDb under this key
CacheDuration string No "00:55:00" TimeSpan string for the cached token's lifetime; only used with CacheKey

The in-memory token lives on the adapter instance. Typed clients registered through AddServiceAdapter<T>() (and the generated helpers) are transient, so without CacheKey a new adapter instance requests a fresh token. With CacheKey, the adapter must declare [AdapterDependency(typeof(ICacheDb))] (KAD0004 otherwise) and capture the injected cache in a field named _cacheDb — the generated token code reads it through that name.

[AdapterDependency] — Class-level (AllowMultiple)

Declares additional DI services the adapter depends on (beyond settings and HttpClient). The generator uses these for documentation and to validate the OAuth cache setup; you still inject them via your constructor.

[AdapterDependency(typeof(ICacheDb))]
[AdapterDependency(typeof(IHostEnvironment))]
public partial class MyAdapter : AbstractServiceAdapter { ... }

[AdapterBaseUrl], [AdapterPing], [AdapterDisablePing] — Class-level

These attributes move common constructor-only configuration into source metadata, which makes C# primary-constructor adapters ergonomic:

[AdapterAuth(AuthScheme.Bearer, SettingsType = typeof(MySettings), TokenProperty = nameof(MySettings.ApiToken))]
[AdapterBaseUrl(nameof(MySettings.ServiceUrl))]
[AdapterDisablePing]
public partial class MyAdapter(HttpClient client, MySettings settings)
    : AbstractServiceAdapter(client, shouldDisposeHttpClient: false)
{
    private readonly MySettings _settings = settings;
}

[AdapterBaseUrl] supplies the default base URL settings property for every endpoint; endpoint-level BaseUrlProperty still wins. The request URL is built as {baseUrl}/{path}, so store base URLs without a trailing slash and write paths without a leading slash. [AdapterPing("health")] configures ping, while [AdapterDisablePing] disables it.

[AdapterJsonSerializerContext] — Class-level

Opts generated JSON endpoints into source-generated System.Text.Json metadata. When present, generated code uses JsonSerializer.Serialize(value, JsonTypeInfo<T>) and JsonSerializer.Deserialize(json, JsonTypeInfo<T>) instead of reflection-based generic serialization. This is the preferred path for trimming and Native AOT.

using System.Text.Json.Serialization;

[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(TrackingRequest))]
[JsonSerializable(typeof(TrackingResponse))]
internal partial class TrackingAdapterJsonSerializerContext : JsonSerializerContext
{
}

[AdapterAuth(AuthScheme.Bearer, SettingsType = typeof(TrackingSettings), TokenProperty = nameof(TrackingSettings.Token))]
[AdapterJsonSerializerContext(typeof(TrackingAdapterJsonSerializerContext))]
public partial class TrackingAdapter : AbstractServiceAdapter
{
    [AdapterEndpoint(HttpVerb.Post, "tracking/v1/events")]
    public virtual partial Task<TrackingResponse?> TrackAsync(TrackingRequest request);
}

The KAD0008 analyzer keeps this maintainable. If the adapter has JSON endpoints but no context, the code fix creates {AdapterName}JsonSerializerContext, adds [AdapterJsonSerializerContext(typeof(...))], and includes [JsonSerializable] entries for every JSON request, response, and response-wrapper type. If a context already exists, the code fix adds only the missing [JsonSerializable] entries.

Hand-written overrides and custom adapter methods can use the same AOT-safe metadata path through the runtime helpers:

public override async Task<TrackingResponse?> TrackAsync(TrackingRequest request)
{
    using var response = await PostAsync(
        "tracking/v1/events",
        request,
        TrackingAdapterJsonSerializerContext.Default.TrackingRequest);

    return await response.ReceiveJsonAsync(TrackingAdapterJsonSerializerContext.Default.TrackingResponse);
}

Enums

HttpVerb

Every generated endpoint builds an HttpRequestMessage and sends it with ServiceAdapterHttpClient.SendAsync, passing the method's CancellationToken parameter (or CancellationToken.None when it has none).

Value Ordinal Request
Get 0 HttpMethod.Get
Post 1 HttpMethod.Post with the request body
Put 2 HttpMethod.Put with the request body
Delete 3 HttpMethod.Delete
Patch 4 HttpMethod.Patch with the request body

AuthScheme

Auth headers are attached to the individual request, never to HttpClient.DefaultRequestHeaders, so they cannot leak across calls.

Value Ordinal What the generator emits
None 0 No auth headers
Bearer 1 Authorization: Bearer {settings.TokenProperty} (or an OAuth token when [OAuthClientCredentials] is present and no TokenProperty is set)
Basic 2 Authorization: Basic base64(username:password) from the settings properties
ApiKey 3 One header per HeaderMappings entry
OAuth 4 Authorization: Bearer {await GetOrRefreshAccessTokenAsync()}, plus any HeaderMappings headers
Identity 5 Reserved; no header is emitted

ErrorBehavior

For every non-success response the generated method first awaits OnEndpointErrorAsync(methodName, response), then:

Value Ordinal What the generator emits
Throw 0 Throws ApiServiceException with ErrorMessage (default "{MethodName} failed")
StatusSwitch 1 Awaits virtual OnStatusCodeErrorAsync(methodName, statusCode, response); the default implementation throws ApiServiceException
Custom 2 Nothing more — your OnEndpointErrorAsync override decides

With StatusSwitch and Custom, if your hook returns without throwing, the generated method continues and deserializes the response body. Throw from the hook for failures, and use NotFoundReturnsNull rather than a hook to map 404 to null.

RequestPayloadContentType

Value Ordinal Serializer
Json 1 System.Text.Json; uses JsonTypeInfo<T> when [AdapterJsonSerializerContext] is declared, otherwise the compatibility options fallback
Xml 2 XmlSerializer via generated SerializeToXmlContent / DeserializeXmlResponseAsync<T>

Generated Files

For each annotated adapter class, the generator produces:

File Condition Contents
{Assembly}.{Namespace}.{ClassName}.Endpoints.g.cs Always virtual method bodies for every [AdapterEndpoint] method, plus OnEndpointSuccessAsync / OnEndpointErrorAsync hooks (and OnStatusCodeErrorAsync when any endpoint uses StatusSwitch)
{Assembly}.{Namespace}.{ClassName}.OAuth.g.cs [OAuthClientCredentials] present SemaphoreSlim-protected GetOrRefreshAccessTokenAsync() with double-check locking
{Assembly}.AdapterRegistration.g.cs Once per assembly with adapters AddGeneratedServiceAdapters() plus independent Add{AdapterName}() helpers, all backed by AddServiceAdapter<T>()
{Assembly}.AdapterSchema.g.cs Once per assembly with adapters Markdown schema doc embedded as AssemblyMetadata, extracted to .adapterschema.md post-build
{Assembly}.AdapterOpenApi.g.cs Once per assembly with adapters OpenAPI 3.1 JSON embedded as AssemblyMetadata, extracted to .adapteropenapi.json post-build

Generated artifact names are sanitized and include the adapter namespace for per-adapter files, so duplicate adapter class names in different namespaces can coexist.

The registration helpers are generated into the Microsoft.Extensions.DependencyInjection namespace. Register all generated adapters in one call:

services.AddGeneratedServiceAdapters();

The generated extension accepts the same optional ServiceAdapterSettings callback and applies it to each adapter:

services.AddGeneratedServiceAdapters(options =>
{
    options.ServiceAdapterMaximumRetryCount = 3;
});

You can also register one generated adapter at a time with the named helpers:

services.AddCourierAdapter();
services.AddLabelPrintAdapter(options =>
{
    options.DisableRetryPolicy = true;
});

When two adapters share the same class name in different namespaces, the named helpers include the namespace prefix, such as AddAlphaTrackingAdapter() and AddBetaTrackingAdapter().

Usage Examples

Bearer Token Auth

[AdapterAuth(AuthScheme.Bearer,
    SettingsType = typeof(PaymentSettings),
    TokenProperty = nameof(PaymentSettings.SecretKey))]
[AdapterBaseUrl(nameof(PaymentSettings.BaseUrl))]
[AdapterDisablePing]
public partial class PaymentAdapter(HttpClient httpClient, PaymentSettings settings)
    : AbstractServiceAdapter(httpClient, shouldDisposeHttpClient: false)
{
    private readonly PaymentSettings _settings = settings;

    [AdapterEndpoint(HttpVerb.Post, "transactions/initialize")]
    public virtual partial Task<TransactionResponse?> InitializeTransactionAsync(InitializeRequest request);

    [AdapterEndpoint(HttpVerb.Get, "transactions/verify/{Reference}")]
    public virtual partial Task<TransactionResponse?> VerifyTransactionAsync(VerifyRequest request);
}

Basic Auth + XML

[AdapterAuth(AuthScheme.Basic,
    SettingsType = typeof(ShippingSettings),
    UsernameProperty = nameof(ShippingSettings.Username),
    PasswordProperty = nameof(ShippingSettings.Password))]
[AdapterDisablePing]
public partial class ShippingAdapter(HttpClient httpClient, ShippingSettings settings)
    : AbstractServiceAdapter(httpClient, shouldDisposeHttpClient: false)
{
    private readonly ShippingSettings _settings = settings;

    [AdapterEndpoint(HttpVerb.Post, "routeDeliveryCreatePreadviceAndLabel",
        ContentType = RequestPayloadContentType.Xml,
        BaseUrlProperty = nameof(ShippingSettings.RoutingUrl),
        ErrorMessage = "Routing request failed")]
    public virtual partial Task<RoutingResponse?> CreateLabelAsync(RoutingRequest request);
}

API Key Auth with Custom Headers

[AdapterAuth(AuthScheme.ApiKey,
    SettingsType = typeof(CourierSettings),
    HeaderMappings = ["api-user:ApiUser", "api-token:ApiToken"])]
[AdapterBaseUrl(nameof(CourierSettings.ServiceUrl))]
[AdapterDisablePing]
public partial class CourierAdapter(HttpClient httpClient, CourierSettings settings)
    : AbstractServiceAdapter(httpClient, shouldDisposeHttpClient: false)
{
    private readonly CourierSettings _settings = settings;

    [AdapterEndpoint(HttpVerb.Get, "api/couriers")]
    public virtual partial Task<List<Courier>?> GetCouriersAsync();

    [AdapterEndpoint(HttpVerb.Post, "api/labels",
        ErrorBehavior = ErrorBehavior.StatusSwitch)]
    public virtual partial Task<LabelResponse?> CreateLabelAsync(LabelRequest request);
}

OAuth2 Client Credentials

[AdapterAuth(AuthScheme.OAuth,
    SettingsType = typeof(LabelPrintSettings),
    HeaderMappings = ["apikey:ApiKey"])]
[OAuthClientCredentials(
    SettingsType = typeof(LabelPrintSettings),
    TokenUrlProperty = nameof(LabelPrintSettings.TokenUrl),
    ClientIdProperty = nameof(LabelPrintSettings.ClientId),
    ClientSecretProperty = nameof(LabelPrintSettings.ClientSecret),
    BufferSeconds = 30)]
[AdapterDisablePing]
public partial class LabelPrintAdapter(HttpClient httpClient, LabelPrintSettings settings)
    : AbstractServiceAdapter(httpClient, shouldDisposeHttpClient: false)
{
    private readonly LabelPrintSettings _settings = settings;

    [AdapterEndpoint(HttpVerb.Post, "print/v1/references",
        BaseUrlProperty = nameof(LabelPrintSettings.ServiceUrl))]
    public virtual partial Task<List<PrintReference>?> CreateReferenceAsync(PrintRequest request);
}

The generator produces GetOrRefreshAccessTokenAsync() in a separate partial file with SemaphoreSlim double-check locking, proactive refresh (default 60s buffer), and DateTime.UtcNow-based expiry tracking.

To share tokens across adapter instances and processes, add a cache key and the ICacheDb dependency:

using Kanject.Core.CacheDb.Abstractions;

[AdapterAuth(AuthScheme.OAuth, SettingsType = typeof(LabelPrintSettings))]
[OAuthClientCredentials(
    SettingsType = typeof(LabelPrintSettings),
    TokenUrlProperty = nameof(LabelPrintSettings.TokenUrl),
    ClientIdProperty = nameof(LabelPrintSettings.ClientId),
    ClientSecretProperty = nameof(LabelPrintSettings.ClientSecret),
    CacheKey = "oauth:label-print",
    CacheDuration = "00:55:00")]
[AdapterDependency(typeof(ICacheDb))]
[AdapterDisablePing]
public partial class CachedLabelPrintAdapter(HttpClient httpClient, LabelPrintSettings settings, ICacheDb cacheDb)
    : AbstractServiceAdapter(httpClient, shouldDisposeHttpClient: false)
{
    private readonly LabelPrintSettings _settings = settings;
    private readonly ICacheDb _cacheDb = cacheDb;   // name read by the generated token code
}

Response Wrapper Unwrapping

When an API wraps responses in an envelope (e.g. { "data": { ... } }):

[AdapterEndpoint(HttpVerb.Post, "transactions/initialize",
    ResponseWrapper = typeof(DataEnvelope<>),
    UnwrapProperty = "Data")]
public virtual partial Task<TransactionData?> InitializeAsync(InitializeRequest request);

The generated code deserializes DataEnvelope<TransactionData> then returns wrapper?.Data.

Additional Dependencies

[AdapterAuth(AuthScheme.Bearer, SettingsType = typeof(MySettings), TokenProperty = "Token")]
[AdapterDependency(typeof(ICacheDb))]
[AdapterDependency(typeof(ILogger<MyAdapter>))]
public partial class MyAdapter(
    HttpClient httpClient,
    MySettings settings,
    ICacheDb cacheDb,
    ILogger<MyAdapter> logger) : AbstractServiceAdapter(httpClient, shouldDisposeHttpClient: false)
{
    private readonly MySettings _settings = settings;
    private readonly ICacheDb _cacheDb = cacheDb;
    private readonly ILogger<MyAdapter> _logger = logger;

    [AdapterEndpoint(HttpVerb.Get, "api/items")]
    public virtual partial Task<List<Item>?> GetItemsAsync();
}

Overriding Generated Methods

All generated methods are virtual. Override when you need custom behaviour:

public override async Task<OrderResponse?> CreateOrderAsync(
    CreateOrderRequest request, CancellationToken cancellationToken)
{
    _logger.LogInformation("Creating order for {CustomerId}", request.CustomerId);

    var result = await base.CreateOrderAsync(request, cancellationToken);

    if (result is not null)
        await _orderCache.InvalidateAsync(request.CustomerId);

    return result;
}

Response hooks

Every adapter with endpoints gets two overridable hooks: OnEndpointSuccessAsync(string methodName, HttpResponseMessage response) after a successful response (including a NotFoundReturnsNull short-circuit), and OnEndpointErrorAsync(string methodName, HttpResponseMessage response) for every non-success response. With ErrorBehavior.Custom, OnEndpointErrorAsync is the whole error policy:

protected override async Task OnEndpointErrorAsync(string methodName, HttpResponseMessage response)
{
    var body = await response.Content.ReadAsStringAsync();
    _logger.LogError("Endpoint {Method} failed ({Status}): {Body}",
        methodName, response.StatusCode, body);
    throw new ApiServiceException($"{methodName} failed: {body}");
}

When using ErrorBehavior.StatusSwitch, the generator emits virtual OnStatusCodeErrorAsync:

protected override Task OnStatusCodeErrorAsync(
    string methodName, HttpStatusCode statusCode, HttpResponseMessage response)
{
    if (statusCode == HttpStatusCode.TooManyRequests)
        throw new ApiServiceException($"{methodName} was rate limited");

    throw new ApiServiceException($"{methodName} failed with {statusCode}");
}

Keep in mind that OnEndpointErrorAsync runs for every endpoint of the class, whatever its ErrorBehavior, so an override that throws applies to all of them.

Per-Endpoint Auth Override

Override the class-level auth scheme on a specific endpoint:

// Class uses OAuth, but this endpoint needs no auth
[AdapterEndpoint(HttpVerb.Get, "api/public/health", Auth = AuthScheme.None)]
public virtual partial Task<HealthResponse?> HealthCheckAsync();

An override reuses the class-level [AdapterAuth] settings mappings, so overriding to Basic requires UsernameProperty / PasswordProperty on the class attribute, Bearer requires TokenProperty, and ApiKey requires HeaderMappings.

Route Parameters

Curly-brace placeholders in the path are resolved from the request object's properties:

// {CourierKey} is resolved as request.CourierKey
[AdapterEndpoint(HttpVerb.Post, "api/couriers/{CourierKey}/labels")]
public virtual partial Task<LabelResponse?> CreateLabelAsync(CreateLabelRequest request);

Adapter Schema Documentation

After build, the package's MSBuild targets write {AssemblyName}.adapterschema.md and {AssemblyName}.adapteropenapi.json to your project root. The Markdown document contains:

  • Adapter overview table (name, auth scheme, endpoint count, OAuth/XML flags)
  • Per-adapter detail sections (namespace, auth config, endpoints table)
  • Generated method signatures reference
  • Attribute reference guide, including [AdapterJsonSerializerContext]
  • Type schema catalog and sample request/response payloads

The OpenAPI companion is importable into Postman, Insomnia, Swagger UI, Redoc, NSwag, and other OpenAPI 3.1 tooling.

Extraction reads the generated sources from the intermediate output directory, so while either artifact is enabled the targets set EmitCompilerGeneratedFiles to true unless you set it yourself. dotnet clean deletes both artifacts.

Customize the output location:

<PropertyGroup>
    <AdapterSchemaOutputDir>docs</AdapterSchemaOutputDir>
    <AdapterOpenApiOutputDir>docs/openapi</AdapterOpenApiOutputDir>
</PropertyGroup>

Disable schema generation:

<PropertyGroup>
    <GenerateAdapterSchema>false</GenerateAdapterSchema>
    <GenerateAdapterOpenApi>false</GenerateAdapterOpenApi>
</PropertyGroup>

Diagnostics

Severities can be raised through .editorconfig, for example dotnet_diagnostic.KAD0001.severity = warning.

ID Severity Meaning Code fix
KAD0001 Info Endpoint is missing XML <summary> docs. Add endpoint XML summary.
KAD0002 Info A sample payload in the generated schema fell back to a placeholder; add an XML <example>. —
KAD0003 Error NotFoundReturnsNull = true requires Task<T?>. Change return type to nullable Task<T?>.
KAD0004 Error OAuth CacheKey requires [AdapterDependency(typeof(ICacheDb))]. Add ICacheDb adapter dependency.
KAD0005 Error Query/header template token cannot be resolved. Replace with closest matching token when available.
KAD0006 Error OAuth cache duration is not a positive TimeSpan. Normalize to a valid duration.
KAD0007 Error Primary-constructor settings/cache parameter is not captured in a field. Add the readonly field.
KAD0008 Info JSON endpoints have no serializer context, or the context is missing payload metadata. Create/update the adapter JsonSerializerContext.

Requirements

  • The adapter class must be partial (methods on a non-partial class are ignored by the generator, and the compiler then reports the missing partial implementation)
  • Annotated methods must be partial
  • The adapter class must not be sealed — generated endpoints and hooks are virtual
  • The class must extend AbstractServiceAdapter (directly or indirectly)
  • Settings fields are resolved by matching the SettingsType from [AdapterAuth] or [OAuthClientCredentials] against field types on the class
  • The first parameter that is not a CancellationToken is the request object: it supplies route values, and it is the body for Post, Put and Patch
  • For Native AOT/trimming, prefer [AdapterJsonSerializerContext] so generated JSON endpoints pass JsonTypeInfo<T> at every serialization callsite; use the PostAsync/PutAsync/ReceiveJsonAsync JsonTypeInfo<T> overloads in hand-written overrides
Package Role Availability
Kanject.Core.Adapter Runtime (AbstractServiceAdapter, AddServiceAdapter<T>) and the adapter attributes; depends on this package nuget.org
Kanject.Core.Api.Abstractions ApiServiceException thrown by generated endpoints nuget.org
Kanject.Core.CacheDb.Abstractions ICacheDb for cross-instance OAuth token caching 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.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Kanject.Core.Adapter.Annotations:

Package Downloads
Kanject.Core.Adapter

Kanject service adapter library

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.9.0 45 10/2/2026
1.8.1 136 9/27/2026
1.8.0 98 9/27/2026
1.7.7 109 9/26/2026
1.7.6 147 9/7/2026
1.7.5 123 8/27/2026
1.7.4 130 8/22/2026
1.7.3 154 8/10/2026
1.7.2 125 8/9/2026
1.7.1 137 8/5/2026
1.7.0 138 8/5/2026
1.6.0 140 8/3/2026
1.5.5 144 7/30/2026
1.5.4 151 7/18/2026
1.5.3 152 7/13/2026
1.5.2 139 7/11/2026
1.5.1 157 7/11/2026
1.5.0 208 7/9/2026
1.4.1 140 7/9/2026