Dynamic Routes
  • 17 Apr 2025
  • 9 Minutes to read

Dynamic Routes


Article summary

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:

  1. Tokenized Route Configuration — allows route templates in controller attributes to include placeholders like [route:token?fallback], resolved from external configuration sources.
  2. 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

EndpointRoute ConstantTokens Used
AuthorizationPath.Authorizeauthorize, base
Pushed Authorization RequestPath.PushAuthorizationRequestpar, base
UserInfoPath.UserInfouserinfo, base
EndSessionPath.EndSessionendsession, base
CheckSessionPath.CheckSessionchecksession, base
BackChannel AuthenticationPath.BackChannelAuthenticationbc_authorize, base

TokenController

EndpointRoute ConstantTokens Used
TokenPath.Tokentoken, base
RevocationPath.Revocationrevoke, base
IntrospectionPath.Introspectionintrospect, base

ClientManagementController

EndpointRoute ConstantTokens Used
RegisterPath.Registerregister, base

DiscoveryController

EndpointRoute ConstantTokens Used
ConfigurationPath.Configurationconfiguration, well_known
JWKS KeysPath.Keysjwks, well_known

Token Reference Map

TokenDefault Value (Fallback)Description
base~/connectCommon prefix for most endpoints
authorize[route:base]/authorizeAuthorization endpoint
par[route:base]/parPushed Authorization Request
userinfo[route:base]/userinfoUser Info endpoint
endsession[route:base]/endsessionLogout endpoint
checksession[route:base]/checksessionSession iframe endpoint
bc_authorize[route:base]/bc-authorizeCIBA Backchannel Authentication
token[route:base]/tokenToken issuance
revoke[route:base]/revokeToken revocation
introspect[route:base]/introspectToken introspection
register[route:base]/registerDynamic client registration
well_known~/.well-knownPrefix for discovery metadata
configuration[route:well_known]/openid-configurationOIDC metadata path
jwks[route:well_known]/jwksJWKS 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.


Was this article helpful?