From c8a66048e49c829a1fc7cf38cd9ebe924c333743 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Mon, 23 Jun 2025 14:33:07 +0200 Subject: [PATCH 1/5] Switch from nuget to project reference in Yarp.Application --- src/Application/Yarp.Application.csproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Application/Yarp.Application.csproj b/src/Application/Yarp.Application.csproj index f29c6450d..c2f826e1f 100644 --- a/src/Application/Yarp.Application.csproj +++ b/src/Application/Yarp.Application.csproj @@ -13,7 +13,6 @@ - @@ -25,4 +24,8 @@ + + + + From f1e6dbb2ffdceb9b75f178bccbf3d929edd6aad8 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Mon, 23 Jun 2025 16:16:48 +0200 Subject: [PATCH 2/5] Add OutputCache config for the container --- src/Application/Program.cs | 6 +- src/Application/Yarp.Application.csproj | 1 + .../Middlewares/OutputCacheConfig.cs | 122 ++++++++++++++++++ src/ReverseProxy/Yarp.ReverseProxy.csproj | 2 + .../Configuration/OutputCacheConfigTests.cs | 93 +++++++++++++ 5 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs create mode 100644 test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs diff --git a/src/Application/Program.cs b/src/Application/Program.cs index 45a74ae9e..8f5d2c53f 100644 --- a/src/Application/Program.cs +++ b/src/Application/Program.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Yarp.ReverseProxy.Configuration; // Load configuration @@ -26,13 +27,12 @@ // Configure YARP builder.AddServiceDefaults(); -builder.Services.AddServiceDiscovery(); +builder.Services.AddServiceDiscovery() + .AddOutputCache(builder.Configuration.GetSection("OutputCache")); builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")) .AddServiceDiscoveryDestinationResolver(); -Console.WriteLine(builder.Configuration.GetSection("ReverseProxy").Value); - var app = builder.Build(); app.MapReverseProxy(); diff --git a/src/Application/Yarp.Application.csproj b/src/Application/Yarp.Application.csproj index c2f826e1f..949706e02 100644 --- a/src/Application/Yarp.Application.csproj +++ b/src/Application/Yarp.Application.csproj @@ -7,6 +7,7 @@ enable enable yarp + true diff --git a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs new file mode 100644 index 000000000..f0946e564 --- /dev/null +++ b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OutputCaching; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Yarp.ReverseProxy.Configuration; + +/// +/// Configuration for +/// +public class OutputCacheConfig +{ + /// + public long SizeLimit { get; set; } = 100 * 1024 * 1024; + + /// + public long MaximumBodySize { get; set; } = 64 * 1024 * 1024; + + /// + public TimeSpan DefaultExpirationTimeSpan { get; set; } = TimeSpan.FromSeconds(60); + + /// + public bool UseCaseSensitivePaths { get; set; } + + /// + /// Policies that will be added with + /// + public IDictionary NamedPolicies { get; set; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); +} + +/// +/// Configuration for +/// +public class NamedCacheConfig +{ + /// + /// Flag to exclude or not the default policy + /// + public bool ExcludeDefaultPolicy { get; set; } + + /// + public TimeSpan? Duration { get; set; } + + /// + public bool NoCache { get; set; } + + /// + public string[]? VaryByQueryKeys { get; set; } + + /// + public string[]? VaryByHeaders { get; set; } +} + +/// +/// Collections of extensions to configure OutputCache +/// +public static class OutputCacheConfigExtensions +{ + /// + /// Add and configure OuputCache + /// + public static IServiceCollection AddOutputCache(this IServiceCollection services, IConfiguration config) + { + if (config == null) + { + return services; + } + + var outputCacheConfig = config.Get(); + + if (outputCacheConfig != null) + { + services.AddOutputCache(outputCacheConfig); + } + + return services; + } + + // + /// Add and configure OuputCache + /// + public static IServiceCollection AddOutputCache(this IServiceCollection services, OutputCacheConfig config) + { + return services.AddOutputCache(options => + { + options.SizeLimit = config.SizeLimit; + options.MaximumBodySize = config.MaximumBodySize; + options.DefaultExpirationTimeSpan = config.DefaultExpirationTimeSpan; + options.UseCaseSensitivePaths = config.UseCaseSensitivePaths; + + foreach (var policy in config.NamedPolicies) + { + options.AddPolicy(policy.Key, + builder => PolicyBuilder(builder, policy.Value), + policy.Value.ExcludeDefaultPolicy); + } + }); + } + + private static void PolicyBuilder(OutputCachePolicyBuilder builder, NamedCacheConfig policy) + { + if (policy.Duration.HasValue) + builder.Expire(policy.Duration.Value); + + if (policy.NoCache) + builder.NoCache(); + + if (policy.VaryByQueryKeys != null) + builder.SetVaryByQuery(policy.VaryByQueryKeys); + + if (policy.VaryByHeaders != null) + builder.SetVaryByHeader(policy.VaryByHeaders); + } +} diff --git a/src/ReverseProxy/Yarp.ReverseProxy.csproj b/src/ReverseProxy/Yarp.ReverseProxy.csproj index eab27bee7..81e3615f6 100644 --- a/src/ReverseProxy/Yarp.ReverseProxy.csproj +++ b/src/ReverseProxy/Yarp.ReverseProxy.csproj @@ -10,6 +10,8 @@ true README.md yarp;dotnet;reverse-proxy;aspnetcore + true + $(InterceptorsNamespaces);Microsoft.Extensions.Configuration.Binder.SourceGeneration diff --git a/test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs b/test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs new file mode 100644 index 000000000..36c92dbd9 --- /dev/null +++ b/test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Yarp.ReverseProxy.Configuration; + +public class OutputCacheConfigTests +{ + [Fact] + public async Task All_Options_Added() + { + var config = new OutputCacheConfig(); + config.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(1); + config.MaximumBodySize = 10; + config.SizeLimit = 20; + config.UseCaseSensitivePaths = true; + config.NamedPolicies.Add("test1", new NamedCacheConfig { Duration = TimeSpan.FromSeconds(5), ExcludeDefaultPolicy = true }); + config.NamedPolicies.Add("test2", new NamedCacheConfig { Duration = TimeSpan.FromSeconds(15), ExcludeDefaultPolicy = false }); + config.NamedPolicies.Add("test3", new NamedCacheConfig { Duration = TimeSpan.FromSeconds(3), ExcludeDefaultPolicy = true, VaryByHeaders = new[] { "X-SomeHeader" } }); + + var builder = WebApplication.CreateBuilder(); + builder.Services.AddOutputCache(config) + .AddReverseProxy(); + + var app = builder.Build(); + + var policies = app.Services.GetRequiredService(); + var test1 = await policies.GetPolicyAsync("test1"); + var test2 = await policies.GetPolicyAsync("test2"); + var test3 = await policies.GetPolicyAsync("test3"); + + Assert.NotNull(test1); + Assert.NotNull(test2); + Assert.NotNull(test3); + } + + [Fact] + public async Task All_Options_Added_Json() + { + var json = + """ + { + "OutputCache": { + "DefaultExpirationTimeSpan": "00:05:00", + "MaximumBodySize": 10, + "SizeLimit": 20, + "UseCaseSensitivePaths": true, + "NamedPolicies": { + "test1": { + "Duration": "00:05:00", + "ExcludeDefaultPolicy": true + }, + "test2": { + "Duration": "00:15:00", + "ExcludeDefaultPolicy": false + }, + "test3": { + "Duration": "00:03:00", + "ExcludeDefaultPolicy": true, + "VaryByHeaders": [ "X-SomeHeader" ] + } + } + } + } + """; + var configBuilder = new ConfigurationBuilder(); + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + var config = configBuilder.AddJsonStream(stream).Build(); + + var builder = WebApplication.CreateBuilder(); + builder.Services.AddOutputCache(config.GetSection("OutputCache")) + .AddReverseProxy(); + + var app = builder.Build(); + + var policies = app.Services.GetRequiredService(); + var test1 = await policies.GetPolicyAsync("test1"); + var test2 = await policies.GetPolicyAsync("test2"); + var test3 = await policies.GetPolicyAsync("test3"); + + Assert.NotNull(test1); + Assert.NotNull(test2); + Assert.NotNull(test3); + } +} From c1d0f1b7a0ccf1b94cef01302f7d83056b58b496 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Mon, 23 Jun 2025 21:23:56 +0200 Subject: [PATCH 3/5] Apply suggestions from code review Co-authored-by: Miha Zupan --- .../Configuration/Middlewares/OutputCacheConfig.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs index f0946e564..636ab520f 100644 --- a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs +++ b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs @@ -16,7 +16,7 @@ namespace Yarp.ReverseProxy.Configuration; /// /// Configuration for /// -public class OutputCacheConfig +public sealed record OutputCacheConfig { /// public long SizeLimit { get; set; } = 100 * 1024 * 1024; @@ -33,13 +33,13 @@ public class OutputCacheConfig /// /// Policies that will be added with /// - public IDictionary NamedPolicies { get; set; } = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + public IDictionary NamedPolicies { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); } /// /// Configuration for /// -public class NamedCacheConfig +public sealed record NamedCacheConfig { /// /// Flag to exclude or not the default policy @@ -84,7 +84,7 @@ public static IServiceCollection AddOutputCache(this IServiceCollection services return services; } - // + /// /// Add and configure OuputCache /// public static IServiceCollection AddOutputCache(this IServiceCollection services, OutputCacheConfig config) From 2236aba24826c78f7f7df3b06ff2d6ac0ced5c61 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Mon, 23 Jun 2025 21:25:21 +0200 Subject: [PATCH 4/5] Rename NamedCacheConfig.Duration to NamedCacheConfig.ExpirationTime --- src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs index 636ab520f..36ac89330 100644 --- a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs +++ b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs @@ -47,7 +47,7 @@ public sealed record NamedCacheConfig public bool ExcludeDefaultPolicy { get; set; } /// - public TimeSpan? Duration { get; set; } + public TimeSpan? ExpirationTime { get; set; } /// public bool NoCache { get; set; } From f1c7107f27722e41fad24ceeb1b788b2254f6452 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Mon, 23 Jun 2025 21:26:56 +0200 Subject: [PATCH 5/5] No, use ExpirationTimeSpan instead --- .../Configuration/Middlewares/OutputCacheConfig.cs | 6 +++--- .../Configuration/OutputCacheConfigTests.cs | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs index 36ac89330..88638c809 100644 --- a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs +++ b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs @@ -47,7 +47,7 @@ public sealed record NamedCacheConfig public bool ExcludeDefaultPolicy { get; set; } /// - public TimeSpan? ExpirationTime { get; set; } + public TimeSpan? ExpirationTimeSpan { get; set; } /// public bool NoCache { get; set; } @@ -107,8 +107,8 @@ public static IServiceCollection AddOutputCache(this IServiceCollection services private static void PolicyBuilder(OutputCachePolicyBuilder builder, NamedCacheConfig policy) { - if (policy.Duration.HasValue) - builder.Expire(policy.Duration.Value); + if (policy.ExpirationTimeSpan.HasValue) + builder.Expire(policy.ExpirationTimeSpan.Value); if (policy.NoCache) builder.NoCache(); diff --git a/test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs b/test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs index 36c92dbd9..575584f02 100644 --- a/test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs +++ b/test/ReverseProxy.Tests/Configuration/OutputCacheConfigTests.cs @@ -22,9 +22,9 @@ public async Task All_Options_Added() config.MaximumBodySize = 10; config.SizeLimit = 20; config.UseCaseSensitivePaths = true; - config.NamedPolicies.Add("test1", new NamedCacheConfig { Duration = TimeSpan.FromSeconds(5), ExcludeDefaultPolicy = true }); - config.NamedPolicies.Add("test2", new NamedCacheConfig { Duration = TimeSpan.FromSeconds(15), ExcludeDefaultPolicy = false }); - config.NamedPolicies.Add("test3", new NamedCacheConfig { Duration = TimeSpan.FromSeconds(3), ExcludeDefaultPolicy = true, VaryByHeaders = new[] { "X-SomeHeader" } }); + config.NamedPolicies.Add("test1", new NamedCacheConfig { ExpirationTimeSpan = TimeSpan.FromSeconds(5), ExcludeDefaultPolicy = true }); + config.NamedPolicies.Add("test2", new NamedCacheConfig { ExpirationTimeSpan = TimeSpan.FromSeconds(15), ExcludeDefaultPolicy = false }); + config.NamedPolicies.Add("test3", new NamedCacheConfig { ExpirationTimeSpan = TimeSpan.FromSeconds(3), ExcludeDefaultPolicy = true, VaryByHeaders = new[] { "X-SomeHeader" } }); var builder = WebApplication.CreateBuilder(); builder.Services.AddOutputCache(config) @@ -55,15 +55,15 @@ public async Task All_Options_Added_Json() "UseCaseSensitivePaths": true, "NamedPolicies": { "test1": { - "Duration": "00:05:00", + "ExpirationTimeSpan": "00:05:00", "ExcludeDefaultPolicy": true }, "test2": { - "Duration": "00:15:00", + "ExpirationTimeSpan": "00:15:00", "ExcludeDefaultPolicy": false }, "test3": { - "Duration": "00:03:00", + "ExpirationTimeSpan": "00:03:00", "ExcludeDefaultPolicy": true, "VaryByHeaders": [ "X-SomeHeader" ] }