MMLib.OpenApiForYarp 1.1.0

dotnet add package MMLib.OpenApiForYarp --version 1.1.0
                    
NuGet\Install-Package MMLib.OpenApiForYarp -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MMLib.OpenApiForYarp" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MMLib.OpenApiForYarp" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="MMLib.OpenApiForYarp" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MMLib.OpenApiForYarp --version 1.1.0
                    
#r "nuget: MMLib.OpenApiForYarp, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MMLib.OpenApiForYarp@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MMLib.OpenApiForYarp&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=MMLib.OpenApiForYarp&version=1.1.0
                    
Install as a Cake Tool

<img src="./assets/logo.png" alt="MMLib.OpenApiForYarp logo" width="300"/>

MMLib.OpenApiForYarp

NuGet Downloads Publish License: MIT

Aggregate the OpenAPI documentation of your downstream microservices onto a YARP gateway — and serve it through Scalar or Swagger UI.

MMLib.OpenApiForYarp is the spiritual successor to MMLib.SwaggerForOcelot (3.8M+ NuGet downloads), rebuilt for the modern .NET stack: YARP instead of Ocelot, Scalar as the default UI, and Microsoft.OpenApi as the engine (never Swashbuckle in the core). It fetches each downstream service's OpenAPI document at runtime, rewrites its paths to match how the gateway exposes them, and serves a clean per-service (or merged) document — with a fully extensible transformation pipeline.

  • ✅ Path rewriting driven by your existing YARP routes & transforms — no parallel config
  • ✅ One transformed document per cluster at /openapi/{cluster}.json (+ optional merged /openapi/all.json)
  • Scalar (default) and Swagger UI adapters — the core is UI-agnostic
  • ✅ Security-scheme propagation, published-paths filtering, regex include/exclude
  • ✅ First-class, reorderable transformer pipeline (built-ins are themselves swappable transformers)
  • Service discovery (Microsoft.Extensions.ServiceDiscovery / .NET Aspire) support
  • net8.0 and net10.0

Installation

# Core + Scalar UI (recommended)
dotnet add package MMLib.OpenApiForYarp
dotnet add package MMLib.OpenApiForYarp.Scalar

# Core + Swagger UI (opt-in alternative)
dotnet add package MMLib.OpenApiForYarp
dotnet add package MMLib.OpenApiForYarp.SwaggerUI

Quick start

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .AddOpenApiForYarp();

var app = builder.Build();

app.MapReverseProxy();
app.MapOpenApiForYarp();   // /openapi/{cluster}.json  (+ /openapi/all.json when merging)
app.MapScalarForYarp();    // Scalar UI at /scalar

app.Run();

appsettings.json

{
  "ReverseProxy": {
    "Routes": {
      "products-route": {
        "ClusterId": "products-cluster",
        "Match": { "Path": "/api/products/{**catch-all}" },
        "Transforms": [ { "PathPattern": "/products/{**catch-all}" } ]
      },
      "orders-route": {
        "ClusterId": "orders-cluster",
        "Match": { "Path": "/api/orders/{**catch-all}" },
        "Transforms": [ { "PathPattern": "/orders/{**catch-all}" } ]
      }
    },
    "Clusters": {
      "products-cluster": { "Destinations": { "default": { "Address": "https://localhost:5101" } } },
      "orders-cluster":   { "Destinations": { "default": { "Address": "https://localhost:5102" } } }
    }
  },
  "YarpOpenApi": {
    "MergeDocuments": false,
    "Clusters": {
      "products-cluster": { "Title": "Products API", "OpenApiPath": "/openapi/v1.json" },
      "orders-cluster":   { "Title": "Orders API",   "OpenApiPath": "/openapi/v1.json", "AddOnlyPublishedPaths": true }
    }
  }
}

AddOpenApiForYarp() binds the YarpOpenApi section above — no extra wiring needed (you can also configure it in code). Browse to /scalar — each downstream service appears as its own tab, with paths shown exactly as a client calls them through the gateway.

How path rewriting works

The library reads each cluster's YARP route(s) and inverts the path transforms to compute the gateway-facing path for every downstream path.

Downstream path (service) YARP route Aggregated path (gateway)
GET /products/{id} Match /api/products/{**catch-all}, PathPattern: /products/{**catch-all} GET /api/products/{id}
GET /orders/{id} Match /api/orders/{**catch-all}, PathRemovePrefix: /api* GET /api/orders/{id}

Supported path transforms: PathPattern, PathPrefix, PathRemovePrefix, PathSet, and no-transform passthrough. Non-path transforms (headers, query, etc.) never affect the documented paths. Path parameters and catch-all remainders are preserved verbatim.

* Unlike yarp-swagger, the common YARP path transforms are applied out of the box — you don't need to hand-write a transform factory for each route.

Configuration

There are two ways to configure the library (you can mix them):

1. Configuration section. AddOpenApiForYarp() automatically binds the YarpOpenApi section (from appsettings.json, environment variables, or any configuration source) — it sits alongside YARP's own ReverseProxy section, as shown in the quick start. This is the recommended approach.

2. In code. Pass a delegate to configure (or override the bound values) programmatically — no YarpOpenApi section required:

builder.Services
    .AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
    .AddOpenApiForYarp(options =>
    {
        options.MergeDocuments = true;
        options.CacheDuration = TimeSpan.FromSeconds(30);
        options.MergedDocument.Title = "My Gateway";
        options.MergedDocument.RenameDuplicateSchemas = true;

        options.Clusters["products-cluster"] = new YarpOpenApiClusterOptions
        {
            Title = "Products API",
            OpenApiPath = "/openapi/v1.json",
        };
        options.Clusters["orders-cluster"] = new YarpOpenApiClusterOptions
        {
            Title = "Orders API",
            AddOnlyPublishedPaths = true,
            ExcludePaths = ["^/api/orders/internal"],
        };
    });

The delegate runs after the YarpOpenApi section is bound, so it overrides anything from configuration.

Configuration reference

The YarpOpenApi section (and the YarpOpenApiOptions object passed to the code delegate) supports:

Root options

Option Type Default Description
Clusters dictionary {} Per-cluster options, keyed by YARP cluster id.
MergeDocuments bool false Serve a merged document combining every cluster.
MergedDocument object see below info for the merged document (gateway-owned).
CacheDuration TimeSpan 00:01:00 How long a fetched downstream document is cached.
FetchTimeout TimeSpan 00:00:30 Timeout when fetching a downstream document.
DocumentRoutePattern string /openapi/{cluster}.json Route template for per-cluster documents.

Per-cluster options (YarpOpenApi:Clusters:<clusterId>)

Option Type Default Description
Title string? downstream / cluster id Title shown in the UI for this service.
OpenApiPath string /openapi/v1.json Path on the downstream service serving its OpenAPI JSON.
AddOnlyPublishedPaths bool true Keep only paths the gateway actually proxies. Set to false to include every downstream path.
IncludePaths string[]? null Regex patterns; keep only matching gateway paths.
ExcludePaths string[]? null Regex patterns; drop matching gateway paths.
SecurityScheme string? null Keep only this single named security scheme.

Merged document options (YarpOpenApi:MergedDocument)

Option Type Default Description
Title string Gateway API Merged document title.
Version string 1.0.0 Merged document version.
Description string? null Merged document description.
RoutePattern string /openapi/all.json Route serving the merged document.
DocumentName string all Identifier used by UI adapters.
RenameDuplicateSchemas bool false On a same-named schema with different content, rename the colliding one (and rewrite that service's $refs) instead of keeping the first and warning.

Features

Merged document

Set MergeDocuments: true to additionally serve /openapi/all.json combining every cluster; the merged info comes from MergedDocument. Components are unioned by name: identically-shaped schemas (e.g. a shared Money value object exposed by several services) merge silently into one. When two services define the same name with different content, the first is kept and a warning is logged — or, with MergedDocument:RenameDuplicateSchemas: true, the colliding schema is renamed (prefixed with its cluster) and that service's $refs are rewritten, so the merged document stays correct for every service. Path conflicts always keep the first and warn.

Published-paths filter & regex

By default (AddOnlyPublishedPaths: true) any downstream path that isn't reachable through a YARP route is dropped, so the aggregated document only shows what a client can actually call through the gateway. Set AddOnlyPublishedPaths: false to include every downstream path (unmatched paths keep their original downstream form). IncludePaths / ExcludePaths apply regular expressions to the rewritten gateway paths for finer control.

Security propagation

securitySchemes are propagated from each downstream document. Across services they are deduplicated by name (first wins, conflicts warned). Use the per-cluster SecurityScheme to keep only a specific scheme.

Service discovery (.NET Aspire)

When Microsoft.Extensions.ServiceDiscovery is registered, logical destination addresses (e.g. https://products-service) are resolved to real endpoints before the OpenAPI document is fetched — no breaking change for static configuration. See sample/AppHost for an Aspire example.

Extensibility — the transformation pipeline

The built-in steps (path rewrite → security propagation → published-paths filter) are themselves transformers registered first; you can append, reorder, or replace them at three granularities:

builder.Services
    .AddReverseProxy()
    .LoadFromConfig(/* ... */)
    .AddOpenApiForYarp()
    .AddDocumentTransformer<MyDocumentTransformer>()    // whole document
    .AddOperationTransformer<MyOperationTransformer>()  // per operation
    .AddSchemaTransformer<MySchemaTransformer>();        // per schema
// .ClearOpenApiTransformers() drops the built-ins so you can define your own order

Each transformer receives a strongly-typed context (ClusterName, Route, Cluster, Options, Services, and a per-run Items bag):

public sealed class MyDocumentTransformer : IOpenApiDocumentTransformer
{
    public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken ct)
    {
        document.Info!.Description = $"Proxied via {context.ClusterName}.";
        return Task.CompletedTask;
    }
}
One class, both transforms (YARP ITransformFactory parity)

A class that implements both YARP's ITransformFactory (the proxy transform) and this library's IOpenApiDocumentTransformer is wired up from a single registration — keeping the request transform and the documentation transform in sync:

.AddTransformFactory<MyTransformFactory>(); // registers both sides from one type

Architecture — UI-agnostic by design

The core (MMLib.OpenApiForYarp) has no UI dependency. It exposes the documents through IClusterDocumentSource; the .Scalar and .SwaggerUI packages are thin adapters over it. To build your own UI adapter, resolve IClusterDocumentSource and point your UI at each RoutePattern:

var source = app.Services.GetRequiredService<IClusterDocumentSource>();
foreach (var doc in source.GetDocuments())
{
    // doc.Name, doc.Title, doc.RoutePattern  (e.g. /openapi/products-cluster.json)
}
if (source.MergedDocument is { } merged) { /* ... */ }

Comparison

MMLib.SwaggerForOcelot yarp-swagger MMLib.OpenApiForYarp
Gateway Ocelot YARP YARP
OpenAPI engine Swashbuckle / JObject Swashbuckle IDocumentFilter Microsoft.OpenApi object model
Default UI Swagger UI host's Swagger UI Scalar (+ Swagger UI adapter)
Path rewrite template diffing manual factory per transform automatic from YARP transforms
Config source separate SwaggerEndPoints reuses ReverseProxy reuses ReverseProxy
Extensibility ReConfigureUpstreamSwaggerJson (string) ISwaggerTransformFactory typed document/operation/schema pipeline
Service discovery service section Microsoft.Extensions.ServiceDiscovery / Aspire
Target netstandard / various netX net8.0; net10.0

Building from source

The repository uses NUKE:

./build.sh Compile        # build
./build.sh Test           # build + run all tests (net8.0 + net10.0)
./build.sh Pack           # produce NuGet packages in ./artifacts
./build.sh MutationTest   # run Stryker.NET mutation testing on the core library

CI (build + test) and publish (on push to main, publishing only when the version in Directory.Build.props changes) run via the generated GitHub Actions workflows.

Mutation testing

Stryker.NET is configured (stryker-config.json, local tool in .config/dotnet-tools.json). Run it directly with:

dotnet tool restore
dotnet stryker            # mutates src/MMLib.OpenApiForYarp, runs the unit + integration tests

An HTML report is written to StrykerOutput/. Scope a run with e.g. dotnet stryker --mutate "**/PathTransformation/*.cs".

Known limitations (v1)

  • No request aggregation.
  • No dynamic configuration reload.
  • Authenticated "Try it out" is not wired up.

Contributing

Issues and pull requests are welcome. Run ./build.sh Test before submitting; new behavior should come with tests.

License

MIT © Milan Martiniak

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on MMLib.OpenApiForYarp:

Package Downloads
MMLib.OpenApiForYarp.Scalar

Scalar API reference UI adapter for MMLib.OpenApiForYarp. Registers every downstream cluster document as a separate Scalar document.

MMLib.OpenApiForYarp.SwaggerUI

Swagger UI adapter for MMLib.OpenApiForYarp. Exposes every downstream cluster document in the Swagger UI document selector.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 44 6/19/2026
1.0.0 43 6/19/2026