Kanject.Core.Adapter.Annotations
1.10.0
Prefix Reserved
dotnet add package Kanject.Core.Adapter.Annotations --version 1.10.0
NuGet\Install-Package Kanject.Core.Adapter.Annotations -Version 1.10.0
<PackageReference Include="Kanject.Core.Adapter.Annotations" Version="1.10.0" />
<PackageVersion Include="Kanject.Core.Adapter.Annotations" Version="1.10.0" />
<PackageReference Include="Kanject.Core.Adapter.Annotations" />
paket add Kanject.Core.Adapter.Annotations --version 1.10.0
#r "nuget: Kanject.Core.Adapter.Annotations, 1.10.0"
#:package Kanject.Core.Adapter.Annotations@1.10.0
#addin nuget:?package=Kanject.Core.Adapter.Annotations&version=1.10.0
#tool nuget:?package=Kanject.Core.Adapter.Annotations&version=1.10.0
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 theHttpVerb,AuthScheme,ErrorBehaviorandRequestPayloadContentTypeenums, live inKanject.Core.Adapter(namespacesKanject.Core.Adapter.AttributesandKanject.Core.Adapter.Enums). Generated code also callsKanject.Core.AdapterandKanject.Core.Api.Abstractions, so this package is not useful without the runtime. - It is not marked as a development dependency, so
dotnet add packagewrites a plain reference with noIncludeAssetsline. The package ships the generator underanalyzers/dotnet/csand an MSBuild targets file (schema/OpenAPI extraction) under bothbuild/andbuildTransitive/, which is how the targets reach projects that get this package throughKanject.Core.Adapter. - The generator targets
netstandard2.0and runs inside the compiler; the code it emits requiresKanject.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 arevirtual - The class must extend
AbstractServiceAdapter(directly or indirectly) - Settings fields are resolved by matching the
SettingsTypefrom[AdapterAuth]or[OAuthClientCredentials]against field types on the class - The first parameter that is not a
CancellationTokenis the request object: it supplies route values, and it is the body forPost,PutandPatch - For Native AOT/trimming, prefer
[AdapterJsonSerializerContext]so generated JSON endpoints passJsonTypeInfo<T>at every serialization callsite; use thePostAsync/PutAsync/ReceiveJsonAsyncJsonTypeInfo<T>overloads in hand-written overrides
Related packages
| 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.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- Kanject.Core.Annotations (>= 3.15.0)
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.10.0 | 4 | 10/5/2026 |
| 1.9.0 | 51 | 10/2/2026 |
| 1.8.1 | 137 | 9/27/2026 |
| 1.8.0 | 98 | 9/27/2026 |
| 1.7.7 | 110 | 9/26/2026 |
| 1.7.6 | 147 | 9/7/2026 |
| 1.7.5 | 124 | 8/27/2026 |
| 1.7.4 | 131 | 8/22/2026 |
| 1.7.3 | 155 | 8/10/2026 |
| 1.7.2 | 126 | 8/9/2026 |
| 1.7.1 | 137 | 8/5/2026 |
| 1.7.0 | 138 | 8/5/2026 |
| 1.6.0 | 141 | 8/3/2026 |
| 1.5.5 | 145 | 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 |