
YARP (Yet Another Reverse Proxy) is a flexible .NET reverse proxy library that supports loading configuration from multiple custom sources and provides the means to reload its configuration at runtime; no app restart required.
In my previous Configuring YARP Routes article, I covered the key route configuration scenarios that you will most likely need to know about when using YARP for the first time.
In this article, I will cover how to load YARP configuration at runtime and how to implement custom logic for dynamic configuration loading.
Setup
You won’t need much setup to get started. If you’re new to YARP, check out my Getting started with YARP post, which walks through creating your first project and will provide you with a solid foundation. You’re also welcome to review my Configuring YARP Routes article before diving further into configuration.
That being said, aside from an IDE such as Visual Studio or Visual Studio Code, all you really need is an ASP.NET Core project with the YARP NuGet package installed — so let’s get into it!
Runtime Configuration
The simplest way to demonstrate runtime updates to the YARP configuration is by leveraging the built-in InMemoryConfigProvider that ships with the YARP library, along with Minimal API endpoints to expose the configuration update functionality.
Basic example
The Program.cs file below provides a complete example of how to implement this.
using Microsoft.AspNetCore.Mvc; using Yarp.ReverseProxy.Configuration; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddReverseProxy() .LoadFromMemory([], []); var app = builder.Build(); app.MapPut("/config", async ( [FromBody] ConfigUpdateRequest request, HttpContext context, InMemoryConfigProvider configProvider) => { if (request is null) { context.Response.StatusCode = StatusCodes.Status400BadRequest; return; } configProvider.Update(request.Routes.ToList(), request.Clusters.ToList()); context.Response.StatusCode = StatusCodes.Status200OK; }); app.MapReverseProxy(); app.Run(); public record ConfigUpdateRequest { public required IEnumerable<RouteConfig> Routes { get; init; } public required IEnumerable<ClusterConfig> Clusters { get; init; } }
In the above code, after calling the AddReverseProxy method, the LoadFromMemory method is called, passing an empty array for the routes and clusters configuration. This could be changed to load the configuration from a file, database, or other source, depending on requirements.
Note that it’s necessary to call the LoadFromMemory method to register the InMemoryConfigProvider service. There isn’t an alternative way to register InMemoryConfigProvider easily otherwise.
A Minimal API endpoint is configured to handle PUT requests for the ‘/config’ route. The endpoint handler deserialises the request body automatically into a ConfigUpdateRequest record, which is defined at the bottom of the file for convenience as part of the demo code.
Note that for real-world situations, you’ll need to ensure that your endpoints are secured, requiring a JWT Bearer token, for example, before authorising requests. This is especially important for sensitive endpoints like this, where the system configuration can be manipulated.
The Update method on the injected InMemoryConfigProvider is called to update the configuration, passing in the collection of routes and clusters to apply. If everything works as expected, a HTTP 200 (OK) status code will be returned to the client.
Note that for simplicity, the ConfigUpdateRequest record uses the same types that the Update method expects. However, you’ll likely have your own configuration types that will be mapped to the YARP RouteConfig and ClusterConfig types.
That’s really all there is to setting up a basic runtime configuration!
Trying it out
Alright, let’s test what we’ve set up so far.
If we launch the application with the code listed in the previous subsection, we’ll be greeted with a message similar to the following when launching with Google Chrome.
This localhost page can’t be found
No web page was found for the web address: https://localhost:7101/
HTTP ERROR 404
If we try adding a path to the address bar, such as ‘/todos’, a similar message to the one shown above should be displayed, since no routes or clusters have been configured yet.
At this point, let’s send a PUT request to the ‘/config’ Minimal API endpoint with the following JSON body (you can use Postman or a HTTP file in Visual Studio to do this).
{ "Routes": [ { "RouteId": "todos", "ClusterId": "jsonplaceholder", "Match": { "Path": "/todos/" } } ], "Clusters": [ { "ClusterId": "jsonplaceholder", "Destinations": { "default": { "Address": "https://jsonplaceholder.typicode.com/" } } } ] }
If all goes well, you should receive a successful response confirming that the configuration has been applied.
Now, if you try adding the ‘/todos’ path to the end of the base URL that the API is running on, you should find that the request is successfully routed to the JSONPlaceholder API and see a JSON array of Todo objects returned.
Armed with this knowledge, you can already see the possibilities for how you could apply a custom configuration from your own external source.
Custom source example
Before wrapping up, I’d like to give you an idea of how to implement logic for loading configuration from a custom source.
The YARP Extensibility: Configuration Providers page provides guidance on Configuration Providers, and you can check out the implementation of the InMemoryConfigurationProvider class that ships with YARP on GitHub for reference.
Based on the InMemoryConfigurationProvider implementation, here’s a rudimentary example of the structure of a custom Configuration Provider that implements the IProxyConfigProvider interface.
using Microsoft.Extensions.Primitives; using Yarp.ReverseProxy.Configuration; namespace YarpDemo; /// <summary> /// Provides an implementation of IProxyConfigProvider to support config being loaded from a database. /// </summary> public sealed class DatabaseConfigProvider : IProxyConfigProvider { // Marked as volatile so that updates are atomic private volatile DatabaseConfig _config; private readonly IConfigurationRepository _repository; /// <summary> /// Creates a new instance with a configuration repository for database access. /// </summary> public DatabaseConfigProvider(IConfigurationRepository repository) { ArgumentNullException.ThrowIfNull(repository); _repository = repository; // Initialize with empty configuration _config = new DatabaseConfig([], [], Guid.NewGuid().ToString()); } /// <summary> /// Implementation of the IProxyConfigProvider.GetConfig method to supply the current snapshot of configuration /// </summary> /// <returns>An immutable snapshot of the current configuration state</returns> public IProxyConfig GetConfig() => _config; /// <summary> /// Loads the configuration from the database and signals that the old one is outdated. /// </summary> public async Task LoadFromDatabaseAsync() { var (routes, clusters, revisionId) = await _repository.LoadConfigurationAsync(); var newConfig = new DatabaseConfig(routes, clusters, revisionId); UpdateInternal(newConfig); } private void UpdateInternal(DatabaseConfig newConfig) { var oldConfig = Interlocked.Exchange(ref _config, newConfig); oldConfig.SignalChange(); } /// <summary> /// Implementation of IProxyConfig which is a snapshot of the current config state. The data for this class should be immutable. /// </summary> private sealed class DatabaseConfig : IProxyConfig { // Used to implement the change token for the state private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public DatabaseConfig(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId) { ArgumentNullException.ThrowIfNull(revisionId); RevisionId = revisionId; Routes = routes; Clusters = clusters; ChangeToken = new CancellationChangeToken(_cts.Token); } /// <inheritdoc/> public string RevisionId { get; } /// <summary> /// A snapshot of the list of routes for the proxy /// </summary> public IReadOnlyList<RouteConfig> Routes { get; } /// <summary> /// A snapshot of the list of Clusters which are collections of interchangeable destination endpoints /// </summary> public IReadOnlyList<ClusterConfig> Clusters { get; } /// <summary> /// Fired to indicate the proxy state has changed, and that this snapshot is now stale /// </summary> public IChangeToken ChangeToken { get; } internal void SignalChange() { _cts.Cancel(); } } } /// <summary> /// Interface for accessing proxy configuration from the database. /// </summary> public interface IConfigurationRepository { /// <summary> /// Loads routes, clusters, and revision ID from the database. /// </summary> Task<(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)> LoadConfigurationAsync(); } public class ConfigurationRepository : IConfigurationRepository { public Task<(IReadOnlyList<RouteConfig> routes, IReadOnlyList<ClusterConfig> clusters, string revisionId)> LoadConfigurationAsync() { // In a real implementation, this method would access the database to load the configuration. // For this example, we return a static configuration. var routes = new List<RouteConfig> { new RouteConfig { RouteId = "todos", ClusterId = "jsonplaceholder", Match = new RouteMatch { Path = "/todos/" } } }; var clusters = new List<ClusterConfig> { new ClusterConfig { ClusterId = "jsonplaceholder", Destinations = new Dictionary<string, DestinationConfig> { { "default", new DestinationConfig { Address = "https://jsonplaceholder.typicode.com/" } } } } }; var revisionId = Guid.NewGuid().ToString(); return Task.FromResult((routes as IReadOnlyList<RouteConfig>, clusters as IReadOnlyList<ClusterConfig>, revisionId)); } }
In the above example, the key part of the Configuration Provider is the public GetConfig method that the IProxyConfigProvider interface requires, and the methods to load the configuration and signal that the internal state of the configuration has changed.
The DatabaseConfig class implements the IProxyConfig interface, which requires the following properties: RevisionId, Routes, Clusters, and ChangeToken. The ChangeToken is a CancellationChangeToken that is signalled by the Configuration Provider when the configuration changes.
As you can see, there isn’t much difficulty in implementing a custom Configuration Provider when using the InMemoryConfigurationProvider class as an example.
If you do create your own Configuration Provider, instead of calling the LoadMemory method, you’ll need to register your class as an IProxyConfigProvider service in your app startup logic.
Summary
In this article, I have walked through a basic example of how to load and update YARP configuration dynamically at runtime, leveraging the InMemoryConfigurationProvider that ships with YARP to do the heavy lifting.
I also provided some insight into how you can develop your own custom Configuration Provider to load the YARP configuration from another source, providing you with full control of the configuration loading logic.
I trust that you found the information in this post helpful, as always, and let me know in the comments if there are any interesting YARP configuration scenarios that you’ve come across.


Comments