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 f29c6450d..949706e02 100644 --- a/src/Application/Yarp.Application.csproj +++ b/src/Application/Yarp.Application.csproj @@ -7,13 +7,13 @@ enable enable yarp + true - @@ -25,4 +25,8 @@ + + + + diff --git a/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs b/src/ReverseProxy/Configuration/Middlewares/OutputCacheConfig.cs new file mode 100644 index 000000000..88638c809 --- /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 sealed record 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.OrdinalIgnoreCase); +} + +/// +/// Configuration for +/// +public sealed record NamedCacheConfig +{ + /// + /// Flag to exclude or not the default policy + /// + public bool ExcludeDefaultPolicy { get; set; } + + /// + public TimeSpan? ExpirationTimeSpan { 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.ExpirationTimeSpan.HasValue) + builder.Expire(policy.ExpirationTimeSpan.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..575584f02 --- /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 { 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) + .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": { + "ExpirationTimeSpan": "00:05:00", + "ExcludeDefaultPolicy": true + }, + "test2": { + "ExpirationTimeSpan": "00:15:00", + "ExcludeDefaultPolicy": false + }, + "test3": { + "ExpirationTimeSpan": "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); + } +}