Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f69d410
Introduce structured configuration model for YARP container app
davidfowl Apr 11, 2026
1b7105f
Add StaticSite sample and fix config file webroot resolution
davidfowl Apr 11, 2026
7ed994c
Add StaticSite sample to samples README
davidfowl Apr 11, 2026
ee50552
Add startup banner and suppress framework console noise
davidfowl Apr 11, 2026
38b70aa
Fix logging escape hatch: re-add Logging config after default filters
davidfowl Apr 11, 2026
b8938f2
Trim schema, options, and README to implemented features only
davidfowl Apr 11, 2026
d4d9c47
Fix review findings: clear AsyncLocal after use, handle null director…
davidfowl Apr 11, 2026
255ef89
Simplify README intro
davidfowl Apr 11, 2026
65279bd
README: describe as opinionated YARP distribution
davidfowl Apr 11, 2026
4b50b3c
README: mention ASP.NET Core and YARP
davidfowl Apr 11, 2026
ab2aaff
README: reverse proxy first, not static file host
davidfowl Apr 11, 2026
9e5b5c9
README: web server and reverse proxy
davidfowl Apr 11, 2026
bcd0c8e
Remove auto-generated yarp.sln
davidfowl Apr 11, 2026
3098351
Add StaticSite sample to YARP.slnx
davidfowl Apr 11, 2026
5d6d640
Fix markdown lint errors in READMEs
davidfowl Apr 11, 2026
57c78f7
Improve test infrastructure and coverage
davidfowl Apr 11, 2026
56a5513
Remove unnecessary DisableTestParallelization
davidfowl Apr 11, 2026
4d7eead
Simplify AddReverseProxy: use builder.Configuration directly
davidfowl Apr 12, 2026
69ecbb0
Fix outdated links
MihaZupan Apr 15, 2026
1b8d8e4
Replace section name string literals with nameof in YarpAppConfigBinder
Copilot Apr 15, 2026
590969e
Fix PR feedback on container README
davidfowl Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions YARP.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
<Project Path="samples/Prometheus/HttpLoadApp/HttpLoadApp.csproj" />
<Project Path="samples/Prometheus/ReverseProxy.Metrics-Prometheus.Sample/ReverseProxy.Metrics.Prometheus.Sample.csproj" />
</Folder>
<Folder Name="/samples/StaticSite/">
<File Path="samples/StaticSite/README.md" />
<File Path="samples/StaticSite/yarp-config.json" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path="eng/Versions.props" />
Expand Down
1 change: 1 addition & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ The following samples are provided:
| [Lets Encrypt](ReverseProxy.LetsEncrypt.Sample) | Shows how to use a certificate authority such as Lets Encrypt to set up TLS termination in YARP. |
| [Kubernetes Ingress](KubernetesIngress.Sample) | Shows how to use YARP as a Kubernetes ingress controller |
| [Prometheus](Prometheus) | Shows how to consume the YARP telemetry library and export metrics to external telemetry such as Prometheus |
| [Static Site](StaticSite) | Shows how to use the YARP container application as a static file host with SPA fallback |
23 changes: 23 additions & 0 deletions samples/StaticSite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Static Site Sample

A minimal static site served by the YARP container application.

## Run

```bash
# From the repo root
dotnet run --project src/Application/Yarp.Application.csproj -- samples/StaticSite/yarp-config.json
```

Then open http://localhost:5000

## What's in the box

```text
wwwroot/
index.html # Home page
404.html # Custom error page
_astro/main.a1b2c3.css # Hashed asset (simulates Astro build output)
docs/getting-started/index.html # Nested page with directory default document
yarp-config.json # YARP container configuration
```
13 changes: 13 additions & 0 deletions samples/StaticSite/wwwroot/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404 - Page Not Found</title>
<link rel="stylesheet" href="/_astro/main.a1b2c3.css">
</head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="/">Go home</a>
</body>
</html>
10 changes: 10 additions & 0 deletions samples/StaticSite/wwwroot/_astro/main.a1b2c3.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
color: #333;
}
nav { margin-bottom: 2rem; }
nav a { margin-right: 1rem; }
h1 { color: #512bd4; }
12 changes: 12 additions & 0 deletions samples/StaticSite/wwwroot/docs/getting-started/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Getting Started - YARP</title>
<link rel="stylesheet" href="/_astro/main.a1b2c3.css">
</head>
<body>
<h1>Getting Started</h1>
<p>Learn how to use YARP as a static file host and reverse proxy.</p>
</body>
</html>
18 changes: 18 additions & 0 deletions samples/StaticSite/wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YARP Static Site</title>
<link rel="stylesheet" href="/_astro/main.a1b2c3.css">
</head>
<body>
<nav>
<a href="/">Home</a> |
<a href="/docs/getting-started/">Getting Started</a> |
<a href="/about">About</a>
</nav>
<h1>Welcome to YARP</h1>
<p>This is a static site served by the YARP container.</p>
</body>
</html>
11 changes: 11 additions & 0 deletions samples/StaticSite/yarp-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "../../src/Application/yarp-config.schema.json",

"StaticFiles": {
"Enabled": true
},

"NavigationFallback": {
"Path": "/index.html"
}
}
9 changes: 9 additions & 0 deletions src/Application/Configuration/NavigationFallbackOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Yarp.Application.Configuration;

public sealed class NavigationFallbackOptions
{
public string? Path { get; set; }
}
9 changes: 9 additions & 0 deletions src/Application/Configuration/StaticFilesOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Yarp.Application.Configuration;

public sealed class StaticFilesOptions
{
public bool Enabled { get; set; }
}
9 changes: 9 additions & 0 deletions src/Application/Configuration/TelemetryOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Yarp.Application.Configuration;

public sealed class TelemetryOptions
{
public bool UnsafeAcceptAnyCertificate { get; set; }
}
31 changes: 31 additions & 0 deletions src/Application/Configuration/TestConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Extensions.Configuration;

/// <summary>
/// Test hook that allows WebApplicationFactory to inject configuration
/// before the app binds its config model.
/// Uses AsyncLocal to flow state from test to app without shared mutable state.
/// See: https://github.com/dotnet/aspnetcore/issues/37680
/// </summary>
internal static class TestConfiguration
{
internal static readonly AsyncLocal<Action<IConfigurationBuilder>?> _current = new();

public static IConfigurationBuilder AddTestConfiguration(this IConfigurationBuilder configurationBuilder)
{
if (_current.Value is { } configure)
{
configure(configurationBuilder);
_current.Value = null;
}

return configurationBuilder;
}

public static void Create(Action<IConfigurationBuilder> action)
{
_current.Value = action;
}
}
15 changes: 15 additions & 0 deletions src/Application/Configuration/YarpAppConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Yarp.Application.Configuration;

/// <summary>
/// Root configuration for the YARP container application.
/// Bound from IConfiguration at startup — all application code uses this object model.
/// </summary>
public sealed class YarpAppConfig
{
public StaticFilesOptions StaticFiles { get; set; } = new();
public NavigationFallbackOptions NavigationFallback { get; set; } = new();
public TelemetryOptions Telemetry { get; set; } = new();
}
60 changes: 60 additions & 0 deletions src/Application/Configuration/YarpAppConfigBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Configuration;

namespace Yarp.Application.Configuration;

/// <summary>
/// Binds IConfiguration into the YarpAppConfig object model.
/// This is the single conversion point — all application code uses the resulting object model.
/// </summary>
public static class YarpAppConfigBinder
{
public static YarpAppConfig Bind(IConfiguration configuration)
{
var config = new YarpAppConfig();

configuration.GetSection(nameof(config.StaticFiles)).Bind(config.StaticFiles);
configuration.GetSection(nameof(config.NavigationFallback)).Bind(config.NavigationFallback);
configuration.GetSection(nameof(config.Telemetry)).Bind(config.Telemetry);

// Legacy env var support
MapLegacyKeys(configuration, config);

return config;
}

private static void MapLegacyKeys(IConfiguration configuration, YarpAppConfig config)
{
// YARP_ENABLE_STATIC_FILES -> StaticFiles.Enabled
if (!config.StaticFiles.Enabled
&& string.Equals(configuration["YARP_ENABLE_STATIC_FILES"], "true", StringComparison.OrdinalIgnoreCase))
{
config.StaticFiles.Enabled = true;
}

// YARP_DISABLE_SPA_FALLBACK -> NavigationFallback.Path = null
// Legacy: when static files were enabled, SPA fallback defaulted to on.
// If the new config sets NavigationFallback.Path, that takes precedence.
if (config.StaticFiles.Enabled
&& config.NavigationFallback.Path is null
&& !string.Equals(configuration["YARP_DISABLE_SPA_FALLBACK"], "true", StringComparison.OrdinalIgnoreCase))
{
// Legacy behavior: SPA fallback was on by default when static files were enabled
// Only apply if using legacy keys (no explicit NavigationFallback section)
if (!configuration.GetSection(nameof(config.NavigationFallback)).Exists()
&& string.Equals(configuration["YARP_ENABLE_STATIC_FILES"], "true", StringComparison.OrdinalIgnoreCase))
{
config.NavigationFallback.Path = "/index.html";
}
}

// YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE -> Telemetry.UnsafeAcceptAnyCertificate
if (!config.Telemetry.UnsafeAcceptAnyCertificate
&& string.Equals(configuration["YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE"], "true", StringComparison.OrdinalIgnoreCase))
{
config.Telemetry.UnsafeAcceptAnyCertificate = true;
}
}
}
13 changes: 5 additions & 8 deletions src/Application/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,15 @@
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
using static System.Net.WebRequestMethods;
using Yarp.Application.Configuration;

namespace Microsoft.Extensions.Hosting;

// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
public static class Extensions
{
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder, YarpAppConfig config) where TBuilder : IHostApplicationBuilder
{
builder.ConfigureOpenTelemetry();
builder.ConfigureOpenTelemetry(config);

builder.AddDefaultHealthChecks();

Expand All @@ -31,7 +28,7 @@ public static TBuilder AddServiceDefaults<TBuilder>(this TBuilder builder) where
return builder;
}

public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) where TBuilder : IHostApplicationBuilder
public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder, YarpAppConfig config) where TBuilder : IHostApplicationBuilder
{
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);

Expand Down Expand Up @@ -62,7 +59,7 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder) w
.AddOtlpExporter();
});

if (string.Equals(builder.Configuration["YARP_UNSAFE_OLTP_CERT_ACCEPT_ANY_SERVER_CERTIFICATE"], "true", StringComparison.OrdinalIgnoreCase))
if (config.Telemetry.UnsafeAcceptAnyCertificate)
{
// We cannot use UseOtlpExporter() since it doesn't support configuration via OtlpExporterOptions
// https://github.com/open-telemetry/opentelemetry-dotnet/issues/5802
Expand Down
81 changes: 81 additions & 0 deletions src/Application/Features/LoggingFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Yarp.Application.Configuration;

namespace Yarp.Application.Features;

public static class LoggingFeature
{
/// <summary>
/// Suppress noisy framework logs on the console provider in default mode.
/// Other providers (OTEL, etc.) still receive everything.
/// </summary>
public static ILoggingBuilder ConfigureDefaultLogging(this ILoggingBuilder logging, IConfiguration configuration)
{
// Suppress noisy framework logs on console by default
logging.AddFilter<ConsoleLoggerProvider>("Microsoft.AspNetCore", LogLevel.Warning);
logging.AddFilter<ConsoleLoggerProvider>("Microsoft.AspNetCore.DataProtection", LogLevel.None);
logging.AddFilter<ConsoleLoggerProvider>("Microsoft.Hosting.Lifetime", LogLevel.None);
logging.AddFilter<ConsoleLoggerProvider>("Yarp.ReverseProxy", LogLevel.Warning);

// Re-add logging configuration so user overrides in the Logging section
// take precedence over our defaults above
logging.AddConfiguration(configuration.GetSection("Logging"));

return logging;
}

/// <summary>
/// Print startup banner showing what's configured.
/// Written directly to Console so it always shows regardless of log level.
/// </summary>
public static void PrintBanner(YarpAppConfig config, string? configFilePath, WebApplication app)
{
Console.WriteLine();
Console.WriteLine("YARP");
Console.WriteLine();

if (configFilePath is not null)
{
Console.WriteLine($" Config: {configFilePath}");
}

if (config.StaticFiles.Enabled)
{
var webRoot = app.Environment.WebRootPath ?? "(not found)";
Console.WriteLine($" Static files: {webRoot}");
}

if (config.NavigationFallback.Path is not null)
{
Console.WriteLine($" SPA fallback: {config.NavigationFallback.Path}");
}

Console.WriteLine();

// Print listening URLs once the server has started
app.Lifetime.ApplicationStarted.Register(() =>
{
var addresses = app.Services.GetRequiredService<IServer>()
.Features.Get<IServerAddressesFeature>()?.Addresses;

if (addresses is { Count: > 0 })
{
foreach (var address in addresses)
{
Console.WriteLine($" Listening on: {address}");
}
Console.WriteLine();
}
});
}
}
20 changes: 20 additions & 0 deletions src/Application/Features/NavigationFallbackFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;
using Yarp.Application.Configuration;

namespace Yarp.Application.Features;

public static class NavigationFallbackFeature
{
public static WebApplication MapNavigationFallback(this WebApplication app, YarpAppConfig config)
{
if (config.NavigationFallback.Path is not null)
{
app.MapFallbackToFile(config.NavigationFallback.Path);
}

return app;
}
}
Loading
Loading