AddDkPlus is the recommended registration for applications that store DK Plus connections in their own database or secret provider. It resolves an immutable tenant/company/user context and asynchronously retrieves credentials for every API operation.
Application scope
-> IDkPlusClient
-> IDkPlusContextProvider
-> IDkPlusTokenProvider
-> request-level Authorization header
-> credential-free IHttpClientFactory transport
The library has no dependency on ASP.NET Core, Entity Framework Core, Dapper, or a particular tenant system.
using DevOp.DK.Plus.Extensions;
builder.Services.AddDkPlus();
builder.Services.AddScoped<IDkPlusContextProvider, ApplicationDkPlusContextProvider>();
builder.Services.AddScoped<IDkPlusTokenProvider, DatabaseDkPlusTokenProvider>();
AddDkPlus registers IDkPlusClient, IExternalErpClient, and IDkPlusClientFactory as scoped services. The context and token providers belong to the consuming application and may depend on its scoped database context.
var context = new DkPlusContext(
tenantId,
companyId,
userId); // optional
Empty identifiers are rejected. UserId is optional so jobs can use company-level credentials. The application must define whether a user context requires a user credential. A provider must never silently fall back to a company credential unless that behavior is explicitly authorized.
The automatic client asks IDkPlusContextProvider for the current context when each request is sent:
public sealed class ApplicationDkPlusContextProvider : IDkPlusContextProvider
{
public DkPlusContext GetCurrent() =>
currentContext.Value
?? throw new DkPlusContextMissingException();
}
The library does not use IHttpContextAccessor. An ASP.NET Core application can populate its provider from authenticated request data; a message consumer can populate it from an authorized message envelope.
public sealed class DatabaseDkPlusTokenProvider(ApplicationDbContext db)
: IDkPlusTokenProvider
{
public async ValueTask<DkPlusCredentials> GetCredentialsAsync(
DkPlusContext context,
CancellationToken cancellationToken = default)
{
var connection = await db.DkPlusConnections
.AsNoTracking()
.SingleOrDefaultAsync(x =>
x.TenantId == context.TenantId
&& x.CompanyId == context.CompanyId
&& x.UserId == context.UserId
&& x.IsEnabled,
cancellationToken);
if (connection is null)
{
throw new DkPlusConnectionNotFoundException(context);
}
return new DkPlusCredentials(
credentialProtector.Decrypt(connection.EncryptedAccessToken),
expiresAt: connection.TokenExpiresAt);
}
}
DkPlusCredentials defaults to https://api.dkplus.is, so BaseUrl only needs to be returned when a connection intentionally overrides the official endpoint. Access tokens are omitted from JSON serialization and redacted from ToString().
The provider owns:
The provider is called for every operation. It can return a cached credential while still enforcing expiration and revocation policy. Expired credentials returned to the client are rejected before an HTTP request is sent.
Typed APIs remain unchanged:
public sealed class CustomerImport(IExternalErpClient erp)
{
public Task<IReadOnlyList<Customer>> RunAsync(
DateTimeOffset modifiedSince,
CancellationToken cancellationToken) =>
erp.Customer.Query()
.ModifiedSince(modifiedSince)
.ToListAsync(cancellationToken);
}
The consuming service does not retrieve tokens, construct HttpClient, or set authorization headers.
Create one DI scope for each background unit of work. Use IDkPlusClientFactory when no ambient request context exists:
await using var scope = scopeFactory.CreateAsyncScope();
var factory = scope.ServiceProvider.GetRequiredService<IDkPlusClientFactory>();
var client = factory.CreateClient(new DkPlusContext(tenantId, companyId));
var customers = await client.Customer.Query()
.ModifiedSince(lastRun)
.ToListAsync(cancellationToken);
The explicit client remains valid until its DI scope is disposed. Several explicit clients can operate concurrently within one scope; each request retains its immutable context and receives only that context's credentials.
Do not inject scoped clients or factories into singleton hosted services. Inject IServiceScopeFactory, create a scope for each job, and resolve the factory inside it.
Credentials are resolved and attached to the individual HttpRequestMessage. They are never placed on the pooled transport handler or the contextual HttpClient.DefaultRequestHeaders.
The package:
IHttpMessageHandlerFactory for connection pooling;GET, HEAD, and OPTIONS operations with bounded standard resilience defaults;Resilience pipelines are partitioned by redacted endpoint authority. Tenants that share one DK Plus origin share dependency-health and concurrency state but never credentials. Authentication is resolved once per logical operation, before the resilience pipeline, and the same authorized request context is preserved across retry attempts. See HTTP resilience.
The default endpoint policy permits only the official https://api.dkplus.is origin. Applications using another legitimate endpoint can implement IDkPlusEndpointPolicy:
builder.Services.AddSingleton<IDkPlusEndpointPolicy, ApplicationDkPlusEndpointPolicy>();
The client always requires an absolute HTTPS origin without user information, query, fragment, or base path, even when a custom policy is installed.
When AddDkPlus does not specify AcceptLanguage, each contextual client follows CultureInfo.CurrentUICulture. Neutral English is added as the fallback; invariant culture uses en directly. Run ASP.NET Core localization middleware before resolving the scoped client.
builder.Services.AddDkPlus(options =>
{
// Optional process-wide override. Omit this to follow CurrentUICulture.
options.AcceptLanguage = "is-IS";
});
Keep caching inside IDkPlusTokenProvider. A suitable isolated key is:
dkplus:{TenantId}:{CompanyId}:{UserIdOrCompanyScope}
Never include the access token in a key. Use bounded expiration, invalidate on change or revocation, and prevent concurrent misses or refresh storms per credential key. Distributed deployments should coordinate refresh through a distributed lock, lease, or concurrency-controlled database update—not a global lock covering every tenant.
AddDkPlusClient remains available for one static token or a fixed list of startup-known connections. AddScopedDkPlusClient remains available when the application has already resolved a credential synchronously before the client is created. Those registrations capture one connection for the client lifetime and are not the recommended database-backed design.
Do not register the same default connection through AddDkPlus and AddDkPlusClient.