Apricot Framework

ASP.NET Core

Registration, configuration binding, startup validation and replacing the provider.

ApricotFramework.SelfDefinition.AspNetCore wires self-definition into an ASP.NET Core host: registration, configuration binding and startup validation.

dotnet add package ApricotFramework.SelfDefinition.AspNetCore

Registration

using ApricotFramework.SelfDefinition.AspNetCore.Extensions;

builder.Services.AddSelfDefinition(builder.Configuration);

That binds the SelfDefinition configuration section, adds the options validator, and registers ISelfDefinitionProvider as a singleton.

{
  "SelfDefinition": {
    "ExternalBaseAddress": "https://api.example.com/gateway"
  }
}

Two other overloads are available — a different section name, and configuration in code:

builder.Services.AddSelfDefinition(builder.Configuration, "Service:Self");

builder.Services.AddSelfDefinition(options =>
{
    options.ExternalBaseAddress = "https://api.example.com/gateway";
});

Calling AddSelfDefinition twice is harmless: the provider and the validator are registered with TryAdd, so exactly one of each ends up in the container.

Startup validation

ExternalBaseAddress is required, and it is checked when the host starts rather than on the first request that needs a link. A service that cannot describe itself never begins serving traffic.

RejectedBecause
missing, empty or whitespacethere is no address to build links from
not an absolute URL — example.com, https://a relative value cannot identify an origin
a scheme other than http/httpsftp://…, /api, //example.comnot reachable as a web origin
embedded credentials — https://user:pass@…they would be copied into every link
a query string — https://example.com/?a=banything appended lands after the query, and is ignored
a fragment — https://example.com/#xanything appended lands inside the fragment

Accepted, and normalised: any number of trailing slashes, and surrounding whitespace.

Note

/api is rejected by the scheme rule rather than the absolute-URL rule, which reads oddly in a log until you know why. A bare path and a scheme-relative //example.com both parse as perfectly valid absolute URIs under the file scheme, so requiring "absolute" alone would let them through and the service would emit file:// links. The scheme allow-list is what actually stops them.

The failure looks like this, and the process exits without opening a port:

Microsoft.Extensions.Options.OptionsValidationException: ExternalBaseAddress 'ftp://example.com'
uses the 'ftp' scheme, but only http and https are supported. Include the scheme explicitly, for
example 'https://api.example.com'.

Warning

This is a behavioural difference worth planning for if you are moving from a configuration that never set the value. Previously a missing address failed the individual request that needed a link; now it stops the deployment. That is deliberate — a service emitting wrong links is harder to notice, and worse, than one that refuses to start.

In a bare ServiceCollection with no host to run the startup hook, the same validation runs the first time the options are resolved, so the failure surfaces on first use instead.

Reloading

The registered provider reads the options through IOptionsMonitor on every call, so editing appsettings.json changes the address without restarting the host.

The validator runs again on every reload, so a reload is not a way to slip a value past the checks the host applied at startup. A broken edit is refused rather than adopted, and repairing the file recovers without a restart.

Note

Where that refusal surfaces depends on which thread fires the configuration change token, because IOptionsMonitor re-creates the options eagerly in its change callback. Under a file watcher — the normal case — the callback runs on a background thread, the failure there is unobserved, and the next GetExternalBaseAddress() throws OptionsValidationException. If your code calls IConfigurationRoot.Reload() itself, that callback runs inline and the same validation failure comes straight back out of Reload(), wrapped in an AggregateException. Either way the bad value is never served.

Replacing what is registered

ISelfDefinitionProvider is registered with TryAddSingleton, so your own registration wins — in either order:

builder.Services.AddSingleton<ISelfDefinitionProvider, MyProvider>();
builder.Services.AddSelfDefinition(builder.Configuration);

This is the supported route to a request-derived address, if you decide you want one. Before you do, read the warning in the overview: the request's host is client-supplied, and links built from it are only as trustworthy as the caller.

To change where the settings come from while keeping the parsing and trimming, derive from the built-in provider instead and override one method:

public sealed class TenantSelfDefinitionProvider : OptionsAwareSelfDefinitionProvider
{
    public TenantSelfDefinitionProvider(IOptionsMonitor<SelfDefinitionOptions> optionsMonitor)
        : base(optionsMonitor)
    {
    }

    protected override SelfDefinitionOptions GetCurrentOptions()
    {
        return new SelfDefinitionOptions { ExternalBaseAddress = ResolveForCurrentTenant() };
    }
}

Note that a provider registered this way bypasses startup validation — the validator checks the configured options, not whatever your override returns.

On this page