diff --git a/samples/Modules/ModulesCore/Program.cs b/samples/Modules/ModulesCore/Program.cs index 1d8cdc70a..db5fd4049 100644 --- a/samples/Modules/ModulesCore/Program.cs +++ b/samples/Modules/ModulesCore/Program.cs @@ -1,4 +1,5 @@ using System.Web; +using Microsoft.AspNetCore.OutputCaching; using ModulesLibrary; var builder = WebApplication.CreateBuilder(args); @@ -13,9 +14,18 @@ options.RegisterModule("Events"); }); +builder.Services.AddOutputCache(options => +{ + options.AddHttpApplicationBasePolicy(_ => new[] { "browser" }); +}); + var app = builder.Build(); app.UseSystemWebAdapters(); +app.UseOutputCache(); + +app.MapGet("/", () => "Hello") + .CacheOutput(); app.Run(); @@ -24,4 +34,14 @@ class MyApp : HttpApplication protected void Application_Start() { } + + public override string? GetVaryByCustomString(System.Web.HttpContext context, string custom) + { + if (custom == "test") + { + return "blah"; + } + + return base.GetVaryByCustomString(context, custom); + } } diff --git a/samples/Modules/ModulesLibrary/EventsModule.cs b/samples/Modules/ModulesLibrary/EventsModule.cs index a584ed5ad..9fd6cf0f7 100644 --- a/samples/Modules/ModulesLibrary/EventsModule.cs +++ b/samples/Modules/ModulesLibrary/EventsModule.cs @@ -17,7 +17,10 @@ protected override void InvokeEvent(HttpContext context, string name) throw new ArgumentNullException(nameof(context)); } - context.Response.ContentType = "text/plain"; + if (context.CurrentNotification == RequestNotification.BeginRequest) + { + context.Response.ContentType = "text/plain"; + } context.Response.Output.WriteLine(name); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationExtensions.cs index 9c50771a6..374bedcd6 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationExtensions.cs @@ -58,15 +58,6 @@ public static ISystemWebAdapterBuilder AddHttpApplication(this ISystemWebA return builder; } - internal static void UseHttpApplication(this IApplicationBuilder app) - { - if (app.AreHttpApplicationEventsRequired()) - { - app.UseMiddleware(); - app.UseHttpApplicationEvent(ApplicationEvent.BeginRequest); - } - } - internal static void UseHttpApplicationEvent(this IApplicationBuilder app, params ApplicationEvent[] preEvents) => app.UseHttpApplicationEvent(preEvents, Array.Empty()); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationOptions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationOptions.cs index b81947a03..0fb6efd24 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationOptions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationOptions.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Web; using static System.FormattableString; diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationVaryBy.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationVaryBy.cs new file mode 100644 index 000000000..bc526a386 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationVaryBy.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET7_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SystemWebAdapters; + +namespace Microsoft.AspNetCore.OutputCaching; + +public static class HttpApplicationVaryByExtensions +{ + /// + /// Adds an output cache policy to the base policy that will query + /// for values to vary by with the keys supplied by the the . + /// + /// The to add the base policy to. + /// The selector for keys to query given the current . + public static void AddHttpApplicationBasePolicy(this OutputCacheOptions options, Func> keySelector) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(keySelector); + + options.AddBasePolicy(new HttpApplicationVaryByPolicy(keySelector)); + } + + /// + /// Adds a collection of custom keys to query for values to vary by. + /// + /// The to add the values to. + /// The custom keys to vary by value. + public static void AddHttpApplicationVaryByCustom(this OutputCachePolicyBuilder builder, params string[] customKeys) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(customKeys); + + foreach (var custom in customKeys) + { + builder.VaryByValue(context => + { + var value = context.Features.Get()?.Application.GetVaryByCustomString(context, custom); + + return new(custom, value ?? string.Empty); + }); + } + } + + /// + /// Adds a named output cache policy that will query + /// for values to vary by with the keys supplied by the the . + /// + /// The to add the named policy to. + /// The selector for keys to query given the current . + public static void AddHttpApplicationPolicy(this OutputCacheOptions options, string name, Func> keySelector) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(keySelector); + + options.AddPolicy(name, new HttpApplicationVaryByPolicy(keySelector)); + } + + private sealed class HttpApplicationVaryByPolicy : IOutputCachePolicy + { + private readonly Func> _keySelector; + + public HttpApplicationVaryByPolicy(Func> keySelector) + { + _keySelector = keySelector; + } + + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellation) + { + if (context.HttpContext.Features.Get() is { Application: { } app }) + { + foreach (var key in _keySelector(context.HttpContext)) + { + context.CacheVaryByRules.VaryByValues[key] = app.GetVaryByCustomString(context.HttpContext, key) ?? string.Empty; + } + } + + return ValueTask.CompletedTask; + } + + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellation) + => ValueTask.CompletedTask; + + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellation) + => ValueTask.CompletedTask; + } +} +#endif + diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.csproj b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.csproj index 1fafd1659..b6f342e64 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.csproj +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.csproj @@ -1,6 +1,6 @@ - net6.0 + net6.0;net7.0 true true Microsoft.AspNetCore.SystemWebAdapters diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RemoteAppClientPostConfigureOptions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RemoteAppClientPostConfigureOptions.cs index a91b71334..849b1cc98 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RemoteAppClientPostConfigureOptions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/RemoteAppClientPostConfigureOptions.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.DependencyInjection; internal class RemoteAppClientPostConfigureOptions : IPostConfigureOptions { - public void PostConfigure(string name, RemoteAppClientOptions options) + public void PostConfigure(string? name, RemoteAppClientOptions options) { if (options.BackchannelClient is null) { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs index 9b0ac5ab4..1abc25345 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs @@ -44,15 +44,11 @@ internal static void UseSystemWebAdapterFeatures(this IApplicationBuilder app) return; } - app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); - app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); - - app.UseHttpApplication(); } public static void UseSystemWebAdapters(this IApplicationBuilder app) @@ -122,6 +118,13 @@ public Action Configure(Action next) => builder => { builder.UseMiddleware(); + builder.UseMiddleware(); + + if (builder.AreHttpApplicationEventsRequired()) + { + builder.UseMiddleware(); + builder.UseHttpApplicationEvent(ApplicationEvent.BeginRequest); + } next(builder); }; diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs index 94c11b434..b7dcd0efb 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs @@ -56,6 +56,7 @@ public event System.EventHandler ResolveRequestCache { add { } remove { } } public event System.EventHandler UpdateRequestCache { add { } remove { } } public void CompleteRequest() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} public void Dispose() { } + public virtual string GetVaryByCustomString(System.Web.HttpContext context, string custom) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public sealed partial class HttpApplicationState : System.Collections.Specialized.NameObjectCollectionBase { diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs index 18e805dbe..a52b71ec3 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs @@ -62,6 +62,16 @@ public HttpContext Context public void CompleteRequest() => Context.Response.End(); + public virtual string? GetVaryByCustomString(HttpContext context, string custom) + { + if (string.Equals(custom, "browser", StringComparison.OrdinalIgnoreCase)) + { + return context?.Request.Browser.Type; + } + + return null; + } + public event EventHandler? BeginRequest { add => AddEvent(value); diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs index 6f5cac863..02b1d9a11 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs @@ -125,6 +125,7 @@ private static async Task> RunAsync(string action, string eventName }) .ConfigureServices(services => { + services.AddSingleton(new ModuleTestStartup(notifier, action)); services.AddRouting(); services.AddSystemWebAdapters() .AddHttpApplication(options => @@ -137,17 +138,6 @@ private static async Task> RunAsync(string action, string eventName { app.UseRouting(); - app.Use(async (ctx, next) => - { - ctx.Features.Set(notifier); - try - { - await next(ctx); - } - catch (InvalidOperationException) when (action == ModuleTestModule.Throw) - { - } - }); app.UseAuthenticationEvents(); app.UseAuthorizationEvents(); app.UseSystemWebAdapters(); @@ -168,6 +158,37 @@ private sealed class NotificationCollection : List { } + private sealed class ModuleTestStartup : IStartupFilter + { + private readonly NotificationCollection _collection; + private readonly string _action; + + public ModuleTestStartup(NotificationCollection collection, string action) + { + _collection = collection; + _action = action; + } + + public Action Configure(Action next) + => builder => + { + builder.Use(async (ctx, next) => + { + ctx.Features.Set(_collection); + + try + { + await next(ctx); + } + catch (InvalidOperationException) when (_action == ModuleTestModule.Throw) + { + } + }); + + next(builder); + }; + } + private sealed class ModuleTestModule : EventsModule { protected override void InvokeEvent(HttpContext context, string name)