Configuration

An example configuration can be found here in ocelot.json. There are two major sections to the configuration: an array of Routes and a GlobalConfiguration sections:

{
  "Routes": [],
  "GlobalConfiguration": {}
}

From the Getting Started chapter and its Configuration section, you may already know that there are four total configuration sections:

{
  "Routes": [], // static routes
  "DynamicRoutes": [],
  "Aggregates": [], // BFF
  "GlobalConfiguration": {}
}

Section

Description

Routes with Route Schema

The static objects that tell Ocelot how to treat an upstream request. Once static routes have been loaded during gateway startup, in general, they cannot be changed during the lifetime of the app instance, with a few exceptional use cases.

DynamicRoutes with Dynamic Route Schema

This section enables dynamic routing when using a Service Discovery provider. Please refer to the Dynamic Routing 8 docs for more details.

Aggregates with Aggregate Route Schema

This section allows specifying aggregated routes that compose multiple normal routes and map their responses into one JSON object. It allows you to start implementing a Back-end For a Front-end (BFF) type architecture with Ocelot. Please refer to the Aggregation chapter for more details.

GlobalConfiguration with Global Configuration Schema

This section is a bit hacky and allows overrides of static route-specific settings. It is useful if you do not want to manage lots of route-specific settings.

To fully understand all configuration capabilities, we recommend reading all sections below.

Route Schema

Class: FileRoute

Here is the complete route configuration, also known as the “route schema,” of top-level properties. You do not need to set all of these things, but this is everything that is available at the moment.

{
  "AddClaimsToRequest": {}, // dictionary
  "AddHeadersToRequest": {}, // dictionary
  "AddQueriesToRequest": {}, // dictionary
  "AuthenticationOptions": {}, // object
  "ChangeDownstreamPathTemplate": {}, // dictionary
  "DangerousAcceptAnyServerCertificateValidator": false,
  "DelegatingHandlers": [], // array of strings
  "DownstreamHeaderTransform": {}, // dictionary
  "DownstreamHostAndPorts": [], // array of FileHostAndPort
  "DownstreamHttpMethod": "",
  "DownstreamHttpVersion": "",
  "DownstreamHttpVersionPolicy": "",
  "DownstreamPathTemplate": "",
  "DownstreamScheme": "",
  "FileCacheOptions": {}, // object
  "HttpHandlerOptions": {}, // object
  "Key": "",
  "LoadBalancerOptions": {}, // object
  "Metadata": {}, // dictionary
  "Priority": 1, // integer
  "QoSOptions": {}, // object
  "RateLimitOptions": {}, // object
  "RequestIdKey": "",
  "RouteClaimsRequirement": {}, // dictionary
  "RouteIsCaseSensitive": false,
  "SecurityOptions": {}, // object
  "ServiceName": "",
  "ServiceNamespace": "",
  "Timeout": 0, // integer
  "UpstreamHeaderTemplates": {}, // dictionary
  "UpstreamHeaderTransform": {}, // dictionary
  "UpstreamHost": "",
  "UpstreamHttpMethod": [], // array of strings
  "UpstreamPathTemplate": ""
},

The actual route schema with all the properties can be found in the C# FileRoute class.

Dynamic Route Schema

Here is the complete dynamic route configuration, also known as the “dynamic route schema,” of top-level properties.

{
  "DownstreamHttpVersion": "",
  "DownstreamHttpVersionPolicy": "",
  "Metadata": {}, // dictionary
  "RateLimitRule": {},
  "ServiceName": ""
}

The actual dynamic route schema with all the properties can be found in the C# FileDynamicRoute class.

Aggregate Route Schema

Here is the complete aggregated route configuration, also known as the “aggregate route schema,” of top-level properties.

{
  "Aggregator": "",
  "Priority": 1, // integer
  "RouteIsCaseSensitive": false,
  "RouteKeys": [], // array of strings
  "RouteKeysConfig": [], // array of AggregateRouteConfig
  "UpstreamHeaderTemplates": {}, // dictionary
  "UpstreamHost": "",
  "UpstreamHttpMethod": [], // array of strings
  "UpstreamPathTemplate": ""
}

The actual aggregated route schema with all the properties can be found in the C# FileAggregateRoute class.

Global Configuration Schema

Here is the complete global configuration, also known as the “global configuration schema,” of top-level properties.

{
  "BaseUrl": "",
  "CacheOptions": {},
  "DownstreamHttpVersion": "",
  "DownstreamHttpVersionPolicy": "",
  "DownstreamScheme": "",
  "HttpHandlerOptions": {},
  "LoadBalancerOptions": {},
  "MetadataOptions": {},
  "QoSOptions": {},
  "RateLimitOptions": {},
  "RequestIdKey": "",
  "SecurityOptions": {},
  "ServiceDiscoveryProvider": {}
}

The actual global configuration schema with all the properties can be found in the C# FileGlobalConfiguration class.

Configuration Overview

Dependency Injection of the Configuration feature in Ocelot allows you to extend, manage, and build Ocelot Core configuration before the stage of building ASP.NET Core services.

To configure the Ocelot Core and services, use the following abstract program-structure, which must be presented in your Program:

  1. Create application builder: The Microsoft.AspNetCore.Builder.WebApplication has three overloaded versions of the CreateBuilder() methods. Our recommendation is to utilize arguments possibly coming from terminal sessions into an app host; thus, use the CreateBuilder(args) method.

var builder = WebApplication.CreateBuilder(args);
  1. Set up the configuration builder: Utilize the WebApplicationBuilder.Configuration property, which returns a ConfigurationManager object implementing the target IConfigurationBuilder interface.

builder.Configuration.AddOcelot(...);
  1. Forward configuration to the Ocelot builder: The Ocelot.DependencyInjection.ServiceCollectionExtensions class has three overloaded versions of the AddOcelot(IServiceCollection) methods, which return an IOcelotBuilder object.

builder.Services.AddOcelot(builder.Configuration);
  1. Finish the app setup, add middlewares, and finally run the application: Let’s write the final algorithm.

var builder = WebApplication.CreateBuilder(args); // step 1
builder.Configuration.AddOcelot(...); // step 2
builder.Services.AddOcelot(builder.Configuration); // step 3

// Step 4
var app = builder.Build();
await app.UseOcelot();
await app.RunAsync();

For comprehensive documentation of configuration DI-extensions, please refer to the Configuration Overview section in the Dependency Injection chapter.

Multiple Environments

Like any other ASP.NET Core project Ocelot supports configuration file names such as appsettings.dev.json, appsettings.test.json etc. In order to implement this add the following to you:

  var builder = WebApplication.CreateBuilder(args);
  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      .AddJsonFile("ocelot.json") // primary config file
      .AddJsonFile($"ocelot.{builder.Environment.EnvironmentName}.json");
  builder.Services
      .AddOcelot(builder.Configuration);

Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isn’t one. Another version of the configuration above, which is based on configuration providers, is the following:

  var builder = WebApplication.CreateBuilder(args);
  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      .AddOcelot() // single ocelot.json file without environment one
      // or
      .AddOcelot(builder.Environment)
      .AddJsonFile($"ocelot.{builder.Environment.EnvironmentName}.json");
  builder.Services
      .AddOcelot(builder.Configuration);

You also need to set the corresponding ASPNETCORE_ENVIRONMENT variable.

Note 1: More info on configuration can be found in the ASP.NET Core documentation:

Note 2: Calling the following configuration methods is rudimentary in ASP.NET Core because of internal encapsulation in the default builder, aka CreateBuilder(args) method.

  var builder = WebApplication.CreateBuilder(args);
  builder.Configuration
      .AddJsonFile("appsettings.json", true, true) // not required
      .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) // not required
      .AddEnvironmentVariables() // not required
      // ...

This is explained in the Default application configuration sources docs; thus, remove these optional methods.

Merging Files [1]

This feature allows users to have multiple configuration files to make managing large configurations easier.

Rather than directly adding the configuration e.g., using AddJsonFile("ocelot.json"), you can achieve the same result by invoking AddOcelot() as shown below:

  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      .AddOcelot(builder.Environment); // will skip environment file

In this scenario, Ocelot will look for any files that match the pattern ^ocelot\.(.*?)\.json$ as the regular expression and then merge these together. The environment file will be skipped aka ocelot.{builder.Environment.EnvironmentName}.json. If you want to set the GlobalConfiguration property, you must have a file called ocelot.global.json.

The way Ocelot merges the files is basically load them, loop over them, skip environment file, add any Routes, add any AggregateRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any Routes or AggregateRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while Ocelot is running.

Note 1: Currently, validation occurs only during the final merging of configurations in Ocelot. It’s essential to be aware of this when troubleshooting issues. We recommend thoroughly inspecting the contents of the ocelot.json file if you encounter any problems.

Note 2: The Merging feature is operational only during the application’s startup. Consequently, the merged configuration in ocelot.json remains static post-merging and startup. Once the Ocelot application has started, you cannot call the AddOcelot method, nor can you employ the merging feature within AddOcelot. If you still require on-the-fly updating of the primary configuration file, ocelot.json, please refer to the React to Changes section. Additionally, note that merging partial configuration files (such as ocelot.*.json) on the fly using Administration API is not currently implemented.

Note 3: An alternative to static merged configurations could be the construction of the FileConfiguration object before passing it as an argument to the AddOcelot methods method. Refer to the Build From Scratch subsection for details.

Keep files in a folder

You can also give Ocelot a specific path to look in for the configuration files as shown below:

  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      .AddOcelot("/my/folder", builder.Environment); // happy path

Ocelot needs the builder.Environment so it knows to exclude any environment-specific files from the merging algorithm, such as ocelot.{builder.Environment.EnvironmentName}.json.

Merging files to memory [2]

By default, Ocelot writes the merged configuration to disk as ocelot.json (the primary configuration file) by adding the file to the ASP.NET configuration provider.

If your web server lacks write permissions for the configuration folder, you can instruct Ocelot to use the merged configuration directly from memory. Here’s how:

  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      // It implicitly calls ASP.NET AddJsonStream extension method for IConfigurationBuilder
      // .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json)));
      .AddOcelot(builder.Environment, MergeOcelotJson.ToMemory);

This feature proves exceptionally valuable in cloud environments like Azure, AWS, and GCP, especially when the app lacks sufficient write permissions to save files. Furthermore, within Docker container environments, permissions can be scarce, necessitating substantial DevOps efforts to enable file write operations. Therefore, save time by leveraging this feature!

Reload On Change

Ocelot supports reloading the JSON configuration file on change. For instance, the following will recreate Ocelot internal configuration when the ocelot.json file is updated manually:

  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true) // ASP.NET framework version

Note: Starting from version 23.2, most AddOcelot methods include optional bool? arguments, specifically optional and reloadOnChange. Therefore, you have the flexibility to provide these arguments when invoking the native AddJsonFile method during the final configuration step (see AddOcelotJsonFile implementation).

We recommend using the AddOcelot methods to control reloading, rather than relying on the framework’s AddJsonFile method. For example:

  // Old solution based on native framework functionality
  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      .AddJsonFile(ConfigurationBuilderExtensions.PrimaryConfigFile, optional: false, reloadOnChange: true);

  var config = builder.Configuration;
  var env = builder.Environment;
  var mergeTo = MergeOcelotJson.ToFile; // ToMemory
  var folder = "/My/folder";
  var configuration = new FileConfiguration(); // read from anywhere and initialize

  // Advanced solutions based on Ocelot functionality
  config.AddOcelot(env, mergeTo, optional: false, reloadOnChange: true); // with environment and merging type
  config.AddOcelot(folder, env, mergeTo, optional: false, reloadOnChange: true); // with folder, environment and merging type
  config.AddOcelot(configuration, optional: false, reloadOnChange: true); // with configuration object created by your own
  config.AddOcelot(configuration, env, mergeTo, optional: false, reloadOnChange: true); // with configuration object, environment and merging type

Examining the code within the ConfigurationBuilderExtensions class would be helpful for gaining a better understanding of the signatures of the overloaded AddOcelot methods.

React to Changes

Resolve IOcelotConfigurationChangeTokenSource interface from the DI container if you wish to react to changes to the Ocelot configuration via the Administration API or ocelot.json being reloaded from the disk.

You may either poll the change token’s IChangeToken.HasChanged property, or register a callback with the RegisterChangeCallback method.

How to poll is explained here:

public class ConfigurationNotifyingService : BackgroundService
{
    private readonly IOcelotConfigurationChangeTokenSource _tokenSource;
    private readonly ILogger _logger;

    public ConfigurationNotifyingService(IOcelotConfigurationChangeTokenSource tokenSource, ILogger logger)
    {
        _tokenSource = tokenSource;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            if (_tokenSource.ChangeToken.HasChanged)
            {
                _logger.LogInformation("Configuration has changed");
            }
            await Task.Delay(1000, stoppingToken);
        }
    }
}

How to register a callback is explained here:

public sealed class MyConfigurationNotifying : IDisposable
{
    private readonly IOcelotConfigurationChangeTokenSource _tokenSource;
    private readonly IDisposable _callbackHolder;

    public MyConfigurationNotifying(IOcelotConfigurationChangeTokenSource tokenSource)
    {
        _tokenSource = tokenSource;
        _callbackHolder = tokenSource.ChangeToken
            .RegisterChangeCallback(_ => Console.WriteLine("Configuration has changed"), null);
    }

    public void Dispose() => _callbackHolder.Dispose();
}

Store in Consul

As a developer, if you have enabled Service Discovery with Consul support in Ocelot, you may choose to manage your configuration saving to the Consul KV store.

Beyond the traditional methods of storing configuration in a file vs folder (Merging Files 1), or in-memory (Merging files to memory 2), you also have the alternative to utilize the Consul server’s storage capabilities.

For further details on managing Ocelot configurations via a Consul instance, please consult the “Configuration in KV Store” section.

Build From Scratch

Storing, reading, and writing static configurations may have limitations. Therefore, for more flexible and advanced scenarios the FileConfiguration object can be built from scratch in C# code of Ocelot application startup. Additionally after reading static configuration from various sources such as, remote file systems, remote storages or cloudages, you can rewrite options to the configuration.

Ocelot does not provide a fluent syntax to build configuration on fly as other products do. However, it is possible to inject a FileConfiguration object during Ocelot startup using the AddOcelot methods with a special parameter:

public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, /* optional */);

The method above will deserialize the object to disk. If you prefer to keep the configuration in memory, the following method includes the MergeOcelotJson parameter:

public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, /* optional */);

In summary, the final .NET 8+ solution should be written in Program using top-level statements:

  using Ocelot.Configuration.File;
  using Ocelot.DependencyInjection;
  using Ocelot.Middleware;

  var builder = WebApplication.CreateBuilder(args);

  // Build Ocelot's configuration object on the fly:
  var config = new FileConfiguration(); // create new or read static state from anywhere
  // ... initialize or rewrite props: add routes, global config, etc.

  builder.Configuration
      .SetBasePath(builder.Environment.ContentRootPath)
      .AddOcelot(config) // MergeOcelotJson.ToFile : writing config JSON back to disk
      .AddOcelot(config, builder.Environment, MergeOcelotJson.ToMemory); // merging to memory
  builder.Services
      .AddOcelot(builder.Configuration);

  var app = builder.Build();
  await app.UseOcelot();
  await app.RunAsync();

As a final step, you could add shutdown logic to save the complete configuration back to the storage, deserializing it to JSON format.

HttpHandlerOptions

This route configuration section allows for following HTTP redirects, for instance, via the boolean AllowAutoRedirect option. These options can be set at the route or global level.

Use HttpHandlerOptions in a route configuration to set up HttpMessageHandler behavior based on a SocketsHttpHandler instance:

"HttpHandlerOptions": {
  "AllowAutoRedirect": false,
  "MaxConnectionsPerServer": 2147483647, // max value
  "PooledConnectionLifetimeSeconds": null, // integer or null
  "UseCookieContainer": false,
  "UseProxy": false,
  "UseTracing": false
}

Option

Description

AllowAutoRedirect
default: false

This value indicates whether the request should follow Redirection messages (HTTP 3xx status codes). Set it true if the request should automatically follow redirection responses from the downstream resource; otherwise false.

MaxConnectionsPerServer
default: 2147483647, maximum integer

This controls how many connections the internal HttpMessageInvoker will open to a single IIS/Kestrel server.

PooledConnectionLifetimeSeconds
default: 120 seconds

This controls how long a connection can be in the pool to be considered reusable. Also refer to the 1st note below!

UseCookieContainer
default: false

This indicates whether the handler uses the CookieContainer property to store server cookies and uses these cookies when sending requests. Also refer to the 2nd note below!

UseProxy
default: false

Refer to MS Learn: UseProxy Property

UseTracing
default: false

This enables Tracing feature in Ocelot. Also refer to the 3rd note below!

Note 1: If the PooledConnectionLifetimeSeconds option is not defined, the default value is 120 seconds, which is hardcoded in the HttpHandlerOptionsCreator class as the DefaultPooledConnectionLifetimeSeconds constant.

Note 2: If you use the CookieContainer, Ocelot caches the HttpMessageInvoker for each downstream service. This means that all requests to that downstream service will share the same cookies. Issue 274 was created because a user noticed that the cookies were being shared. The Ocelot team tried to think of a nice way to handle this but we think it is impossible. If you don’t cache the clients, that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container, you get race conditions due to inflight requests. This would also mean that subsequent requests don’t use the cookies from the previous response! All in all not a great situation. We would avoid setting UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request!

Note 3: UseTracing option adds a tracing DelegatingHandler (aka Ocelot.Requester.ITracingHandler) after obtaining it from ITracingHandlerFactory, encapsulating the Ocelot.Logging.ITracer service of DI-container.

SSL Errors

If you want to ignore SSL warnings (errors), set the following in your route configuration:

"DangerousAcceptAnyServerCertificateValidator": true

We don’t recommend doing this! The team suggests creating your own certificate and then getting it trusted by your local (or remote) machine, if you can. For https scheme, this fake validator was requested by issue 309. For wss scheme, this fake validator was added by PR 1377.

Note: As a team, we do not consider it an ideal solution. On one hand, the community wants to have an option to work with self-signed certificates. But on the other hand, currently, source code scanners detect two serious security vulnerabilities because of this fake validator in version 20.0 and higher. The Ocelot team will rethink this unfortunate situation, and it is highly likely that this feature will at least be redesigned or removed completely.

For now, the SSL fake validator makes sense in local development environments when a route has https or wss schemes with self-signed certificates for those routes. There are no other reasons to use the DangerousAcceptAnyServerCertificateValidator property at all!

As a team, we highly recommend following these instructions when developing your gateway app with Ocelot:

  • Local development environments: Use this feature to avoid SSL errors for self-signed certificates in the case of https or wss schemes. We understand that some routes should have the downstream scheme exactly with SSL, because they are also in development and/or deployed using SSL protocols. However, we believe that, especially for local development, you can switch from https to http without any objection since the services are in development and there is no risk of data leakage.

  • Remote development environments: Everything is the same as for local development. However, this case is less strict; you have more options to use real certificates to switch off the feature. For instance, you can deploy downstream services to cloud and hosting providers that have their own signed certificates for SSL. At least your team can deploy one remote web server to host downstream services. Install your own certificate or use the cloud provider’s one.

  • Staging or testing environments: We do not recommend using self-signed certificates because web servers should have valid certificates installed. Ask your system administrator or DevOps engineers to create valid certificates.

  • Production environments: Do not use self-signed certificates at all! System administrators or DevOps engineers must create real valid certificates signed by hosting or cloud providers. Switch off the feature for all routes! Remove the DangerousAcceptAnyServerCertificateValidator property for all routes in the production version of the ocelot.json file!

DownstreamHttpVersion

Ocelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as 1.0, 1.1, or 2.0.

DownstreamHttpVersionPolicy [3]

This routing property enables the configuration of the VersionPolicy property within HttpRequestMessage objects for downstream HTTP requests. For additional details, refer to the following documentation:

The DownstreamHttpVersionPolicy option is intricately linked with the DownstreamHttpVersion setting. Therefore, merely specifying DownstreamHttpVersion may sometimes be inadequate, particularly if your downstream services or Ocelot logs report HTTP connection errors such as PROTOCOL_ERROR. In these routes, selecting the precise DownstreamHttpVersionPolicy value is crucial for the HttpVersion policy to prevent such protocol errors.

HTTP2 version policy

Given you aim to ensure a smooth HTTP/2 connection setup for the Ocelot app and downstream services with SSL enabled:

{
  "DownstreamScheme": "https",
  "DownstreamHttpVersion": "2.0",
  "DownstreamHttpVersionPolicy": "", // empty or not defined
  "DangerousAcceptAnyServerCertificateValidator": true
}

And you configure global settings to use Kestrel with this snippet:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureEndpointDefaults(listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http2;
    });
});

When all components are set to communicate exclusively via HTTP/2 without TLS (plain HTTP).

Then the downstream services may display error messages such as:

HTTP/2 connection error (PROTOCOL_ERROR): Invalid HTTP/2 connection preface

To resolve the issue, ensure that HttpRequestMessage has its VersionPolicy set to RequestVersionOrHigher. Therefore, the DownstreamHttpVersionPolicy should be defined as follows:

{
  "DownstreamHttpVersion": "2.0",
  "DownstreamHttpVersionPolicy": "RequestVersionOrHigher" // !
}

Dependency Injection

Dependency Injection for this Configuration feature in Ocelot is designed to extend and/or control the configuration of the Ocelot Core before the stage of building ASP.NET Core pipeline services. The primary methods are AddOcelot methods within the ConfigurationBuilderExtensions class, which offers several overloaded versions with corresponding signatures. You can utilize these methods in the Program.cs file of your gateway app to configure the Ocelot pipeline and services.

Find additional details in the dedicated Configuration Overview section and in subsequent sections related to the Dependency Injection chapter.

Extend with Metadata

Feature: Metadata [4]

The Metadata options can store any arbitrary data that users can access in middlewares, delegating handlers, etc. By using the metadata, users can implement their own logic and extend the functionality of Ocelot.

The Metadata feature is designed to extend both the static Route Schema and Dynamic Route Schema. Global metadata must be defined inside the MetadataOptions section.

The following example demonstrates practical usage of this feature:

{
  "Routes": [
    {
      // other opts...
      "Metadata": {
        "api-id": "FindPost",
        "my-extension/param1": "overwritten-value",
        "other-extension/param1": "value1",
        "other-extension/param2": "value2",
        "tags": "tag1, tag2, area1, area2, func1",
        "json": "[1, 2, 3, 4, 5]"
      }
    }
  ],
  "GlobalConfiguration": {
    // other opts...
    "MetadataOptions": {
      // other metadata opts...
      "Metadata": {
        "instance_name": "dc-1-54abcz",
        "my-extension/param1": "default-value"
      }
    }
  }
}

Note: Route metadata prevails over global metadata from the GlobalConfiguration section. Therefore, if the same key data are defined both at the route and global levels, the route metadata overrides the global ones.

Now, the route metadata can be accessed through the DownstreamRoute object:

using Ocelot.Metadata;

public static class OcelotMiddlewares
{
    public static Task PreAuthenticationMiddleware(HttpContext context, Func<Task> next)
    {
        var route = context.Items.DownstreamRoute();
        var param1 = route.GetMetadata<string>("my-extension/param1") ?? throw new ArgumentNullException("my-extension/param1");
        var param2 = route.GetMetadata<string>("other-extension/param2", "default-value");
        // Working with metadata...
        return next();
    }
}

For comprehensive documentation, please refer to the Metadata chapter.