- 17 Apr 2025
- 9 Minutes to read
- Print
Dynamic Routes
- Updated on 17 Apr 2025
- 9 Minutes to read
- Print
Introduction
Modern cloud-native apps often need flexible and environment-specific routing — especially when deploying to Docker, Kubernetes, or CI/CD pipelines. But with ASP.NET Core’s default static routing model, customizing routes across dev, staging, and production can become error-prone, tedious, and hard to trace.
This article introduces tokenized routing and runtime route resolution as implemented in the Abblix OIDC Server, a certified OpenID Connect library for .NET. These techniques allow you to define route templates using configuration tokens, customize them per environment, and resolve accurate URLs at runtime — all without changing code.
You'll learn:
- Why traditional routing falls short in modern deployments
- How tokenized route templates work and how to configure them
- How to resolve runtime URIs programmatically
- How it all integrates with OpenID Connect discovery and Swagger
Whether you're building secure APIs, multi-tenant SaaS platforms, or enterprise authentication systems — this approach will help you eliminate hardcoded paths, reduce config drift, and simplify environment-specific routing.
Let's dive in.
The Problem We’re Solving
Modern applications often run in multiple environments—development, staging, production, CI/CD pipelines, containerized platforms like Docker, and orchestrators like Kubernetes. Each environment may require different route prefixes or endpoint structures due to varying infrastructure, network policies, or gateway configurations.
Traditionally, routing in ASP.NET Core is configured statically in code via route attributes or configuration files. This rigid approach introduces several problems:
Duplication and Drift: Routes are hardcoded across controllers, tests, OpenID discovery metadata, Swagger docs, and external clients—leading to mismatches and maintenance overhead.
Inflexibility: Customizing routes for different environments often requires code changes, recompilation, and redeployment, which is error-prone and time-consuming.
Lack of Observability: There's no unified way to verify what actual routes the system is using at runtime, making diagnostics and integration testing harder.
Dynamic Routing Overview
The routing system in the Abblix OIDC Server consists of two tightly integrated components:
- Tokenized Route Configuration — allows route templates in controller attributes to include placeholders like
[route:token?fallback]
, resolved from external configuration sources. - Dynamic Route Resolution — provides a runtime mechanism to determine the final, resolved URL of any controller action.
Working together, these components ensure that route configuration is both dynamic and discoverable. Tokenized configuration makes it easy to tailor routing per environment (e.g., development, staging, production), while dynamic resolution ensures consistent output in OpenID Connect discovery metadata and tools like Swagger.
This article explains how the two parts function internally, how to configure them, and how to apply them across various environments including local development, Docker, and Kubernetes.
Who Should Use This
This feature is ideal for:
- Teams deploying the same app across multiple environments (e.g. dev, staging, prod)
- Organizations supporting multi-tenant setups that require dynamic endpoint behavior
- Projects relying on OpenID Connect metadata or Swagger documentation that must reflect accurate, runtime-resolved URIs
Dynamic Routing Details
Routes now can be declared using a custom placeholder syntax:
[route:token?fallback]
Where the token
is the configuration key to look up, and fallback
is used if that key is missing. Tokens can also be nested and resolved recursively.
Here is a real-world example from the Abblix OIDC Server's AuthenticationController
:
[HttpGet("[route:authorize?[route:base?~/connect]/authorize]")]
public async Task<ActionResult> AuthorizeAsync(...) { ... }
This attribute instructs the framework to resolve the route using configuration values. If no configuration is found for authorize
, it falls back to [route:base]/authorize
, which in turn defaults to ~/connect/authorize
.
Given the configuration:
"Routes": {
"base": "~/api",
"authorize": "[route:base]/authorize"
}
The [route:authorize]
token will resolve to [route:base]/authorize
, and finally to ~/api/authorize
.
This demonstrates how tokens can reference other tokens, forming a resolution chain that enables deeply customizable paths. It allows endpoint customization per environment without changing code. It is especially valuable in multi-tenant systems or CI/CD pipelines targeting multiple stages (dev, staging, prod).
How It Works Internally
The ConfigurableRouteConvention
is registered as an IApplicationModelConvention
and executes once during application startup. It applies a custom rule to all MVC controller actions to transform route attributes containing token placeholders like [route:token?fallback]
into final resolved paths.
Under the hood
The resolution process begins by inspecting the ApplicationModel
, which provides metadata for all controllers and actions. For each controller action, the convention checks whether the AttributeRouteModel.Template
contains any [route:
syntax. If so, it uses a regular expression to identify all [route:token?fallback]
patterns, including those nested within other tokens.
Next, it recursively resolves each token using values from the provided IConfigurationSection
. These values can come from appsettings.json
, environment variables, or Kubernetes ConfigMaps. If a token refers to another token, that reference is resolved before substitution.
Once all tokens are resolved, the placeholders are replaced with the final route strings. The updated values are written back into the AttributeRouteModel
, ensuring that the routes used by ASP.NET Core are correct from the beginning. Optionally, the entire resolution process can be logged for observability and troubleshooting.
Behavior Based on Configuration
The behavior of route token resolution depends on whether ConfigureRoutes is called at application startup.
If you call ConfigureRoutes with a specific configuration section, route tokens will be resolved strictly from that configuration. If a token is missing and no fallback is defined, the application will throw a startup exception, helping ensure configuration completeness in production or staging environments. Fallbacks, if provided in the token syntax, are used to fill in missing values.
If you do not call ConfigureRoutes, then the application will skip configuration-based resolution entirely. Instead, it will resolve all tokens using only their fallback values, if defined. This is a safe default for development or environments where full routing configuration is not required.
This mechanism ensures that routing is both flexible and robust, adapting to different stages of deployment without impacting runtime performance.
How Abblix OIDC Server uses it
The Abblix OIDC Server applies tokenized routing to all core endpoints using centralized constants (defined in Path.cs
). These constants incorporate [route:token?fallback]
templates, which are resolved at startup via the ConfigurableRouteConvention
. This approach keeps all route logic consistent and easy to override via configuration.
In production, routes can be overridden via appsettings.json
, environment variables, or Kubernetes ConfigMaps. Regardless of how they’re configured, they are made discoverable to clients through the OpenID Connect metadata manifest.
To ensure runtime consistency, the server uses IEndpointResolver
to dynamically resolve controller action paths. This is especially important for OIDC discovery endpoints, which must accurately reflect real routes used by the system.
Below is a breakdown of route tokens and how they appear across controllers:
Route Reference Summary
Most endpoint routes follow this pattern:
[route:token?[route:base?~/connect]/...]
Where base = ~/connect
by default.
Routes by Controller
AuthenticationController
Endpoint | Route Constant | Tokens Used |
---|---|---|
Authorization | Path.Authorize | authorize , base |
Pushed Authorization Request | Path.PushAuthorizationRequest | par , base |
UserInfo | Path.UserInfo | userinfo , base |
EndSession | Path.EndSession | endsession , base |
CheckSession | Path.CheckSession | checksession , base |
BackChannel Authentication | Path.BackChannelAuthentication | bc_authorize , base |
TokenController
Endpoint | Route Constant | Tokens Used |
---|---|---|
Token | Path.Token | token , base |
Revocation | Path.Revocation | revoke , base |
Introspection | Path.Introspection | introspect , base |
ClientManagementController
Endpoint | Route Constant | Tokens Used |
---|---|---|
Register | Path.Register | register , base |
DiscoveryController
Endpoint | Route Constant | Tokens Used |
---|---|---|
Configuration | Path.Configuration | configuration , well_known |
JWKS Keys | Path.Keys | jwks , well_known |
Token Reference Map
Token | Default Value (Fallback) | Description |
---|---|---|
base | ~/connect | Common prefix for most endpoints |
authorize | [route:base]/authorize | Authorization endpoint |
par | [route:base]/par | Pushed Authorization Request |
userinfo | [route:base]/userinfo | User Info endpoint |
endsession | [route:base]/endsession | Logout endpoint |
checksession | [route:base]/checksession | Session iframe endpoint |
bc_authorize | [route:base]/bc-authorize | CIBA Backchannel Authentication |
token | [route:base]/token | Token issuance |
revoke | [route:base]/revoke | Token revocation |
introspect | [route:base]/introspect | Token introspection |
register | [route:base]/register | Dynamic client registration |
well_known | ~/.well-known | Prefix for discovery metadata |
configuration | [route:well_known]/openid-configuration | OIDC metadata path |
jwks | [route:well_known]/jwks | JWKS public key set |
How to Use
By default, if no explicit call to configure routing is made, fallback values will be used.
Use this during development or testing where no route customization is needed.
Apply the configuration in production or staging by calling the ConfigureRoutes
extension:
services.ConfigureRoutes(Configuration.GetSection("Routes"));
If any required token is missing and no fallback is provided, the application will fail to start. The exception message will identify the unresolved token and indicate the source of the misconfiguration, helping developers pinpoint the issue quickly.
Environment-Specific Configuration
Local Development
"Routes": {
"base": "~/dev",
"authorize": "[route:base]/authorize",
"token": "[route:base]/token",
"userinfo": "[route:base]/userinfo",
"endsession": "[route:base]/endsession",
"revocation": "[route:base]/revoke",
"introspect": "[route:base]/introspect",
"register": "[route:base]/register",
"configuration": "[route:well_known]/openid-configuration",
"jwks": "[route:well_known]/jwks",
"well_known": "~/.well-known"
}
Docker Compose
services:
oidc-server:
image: ...
environment:
Routes__base: "~/docker"
Routes__authorize: "[route:base]/authorize"
Routes__token: "[route:base]/token"
Routes__userinfo: "[route:base]/userinfo"
Routes__endsession: "[route:base]/endsession"
Routes__revocation: "[route:base]/revoke"
Routes__introspect: "[route:base]/introspect"
Routes__register: "[route:base]/register"
Routes__configuration: "[route:well_known]/openid-configuration"
Routes__jwks: "[route:well_known]/jwks"
Routes__well_known: "~/.well-known"
Kubernetes
apiVersion: v1
kind: ConfigMap
metadata:
name: abblix-oidc-config
namespace: default
data:
Routes__base: ~/kube
Routes__authorize: "[route:base]/authorize"
Routes__token: "[route:base]/token"
Routes__userinfo: "[route:base]/userinfo"
Routes__endsession: "[route:base]/endsession"
Routes__revocation: "[route:base]/revoke"
Routes__introspect: "[route:base]/introspect"
Routes__register: "[route:base]/register"
Routes__configuration: "[route:well_known]/openid-configuration"
Routes__jwks: "[route:well_known]/jwks"
Routes__well_known: "~/.well-known"
containers:
- name: oidc-server
image: abblix/oidc-server:latest
envFrom:
- configMapRef:
name: abblix-oidc-config
Runtime URI Resolution
You can also use IEndpointResolver
to resolve final URIs for your controller actions:
var uri = endpointResolver.Resolve("Authentication", "AuthorizeAsync");
Resolution Mechanics
The provided IEndpointResolver
implementation relies on ASP.NET Core's routing infrastructure to identify the actual runtime URLs of controller actions. It starts by querying the EndpointDataSource
, which contains all routes available in the application after MVC conventions and middleware have been applied. From this data source, it filters endpoints based on ControllerActionDescriptor
, matching the controller name and method name using a case-insensitive comparison.
Once the appropriate action is found, it extracts the final route pattern using RoutePattern.RawText
or other metadata that represents the resolved path. This path is then combined with the request's scheme and host information to produce a fully qualified URI. The implementation may use IHttpContextAccessor
or LinkGenerator
for accurate context binding.
The result is a complete URL that reflects the currently active route configuration. This includes routes defined statically, those resolved from tokenized configuration, and those transformed by MVC conventions. This mechanism ensures consistency between runtime behavior and metadata outputs such as OpenID Connect discovery documents and Swagger.
Integration with DiscoveryController
The IEndpointResolver
is already used within the DiscoveryController
to dynamically populate the OpenID Connect metadata manifest. This includes resolved URIs for endpoints such as authorization_endpoint
, token_endpoint
, userinfo_endpoint
, and others.
As a result, the discovery document remains synchronized with the resolved route configuration, which improves client interoperability and reduces risk of mismatches. This design not only simplifies integration with OpenID Connect clients and Swagger tooling, but also enhances the observability and reliability of your API surface.
Why Integration Matters
Integrating ConfigurableRouteConvention
and IEndpointResolver
creates a strong alignment between configuration-time route definition and runtime route resolution. On one side, route templates are resolved at startup based on environment-specific settings. On the other, the resolved routes are programmatically exposed at runtime.
This eliminates the gap between how endpoints are configured and how they're communicated to clients, ensuring alignment between internal behavior and external expectations.
It improves consistency, as OpenID Connect clients, Swagger UI, and other tooling always receive accurate and up-to-date URIs. Developers benefit from cleaner code without hardcoded paths, and operations teams gain confidence that deployments reflect the right routing rules — eliminating surprises and configuration drift.
Together, these capabilities make the server highly adaptable to multi-tenant, multi-environment setups and provide a solid foundation for safe, scalable, and self-documenting API deployments.
Best Practices
To get the most from tokenized routing, define all routes in configuration for consistency and clarity. During development, fallback-only mode provides flexibility, while strict mode should be used in production to catch misconfigurations early.
Instead of hardcoding URLs, always resolve endpoint URIs via IEndpointResolver
. When troubleshooting, review startup logs to trace how route tokens are resolved and which fallbacks (if any) are applied.
Summary
Tokenized routing in Abblix OIDC Server enables fully dynamic, environment-specific route customization without code changes. When combined with IEndpointResolver
, it ensures all routing logic is also discoverable and externally consistent — a must for OpenID Connect metadata and dynamic environments.
Together, these tools support scalable, cloud-native, and standards-compliant deployments across modern infrastructure.