diff --git a/Directory.Build.props b/Directory.Build.props index 3035f70ee5..e813bb8e99 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ enable - 11 + 12 diff --git a/samples/Modules/ModulesFramework/Handler.cs b/samples/Modules/ModulesFramework/Handler.cs index 73d6a41fb2..cfa1121051 100644 --- a/samples/Modules/ModulesFramework/Handler.cs +++ b/samples/Modules/ModulesFramework/Handler.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Web; namespace ModulesFramework @@ -11,6 +8,11 @@ public class Handler : IHttpHandler public void ProcessRequest(HttpContext context) { + context.Response.Write($"State: {context.CurrentNotification}\n"); + context.Response.Flush(); + context.Response.Write($"State: {context.CurrentNotification}\n"); + context.Response.Flush(); + context.Response.Write($"State: {context.CurrentNotification}\n"); } } } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/HttpApplicationFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/HttpApplicationFeature.cs index 87ad5463b3..e5d4fad18d 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/HttpApplicationFeature.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/Features/HttpApplicationFeature.cs @@ -11,6 +11,14 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.Features; internal sealed class HttpApplicationFeature : IHttpApplicationFeature, IHttpResponseEndFeature, IRequestExceptionFeature, IDisposable { + private static readonly HashSet _suppressThrow = + [ + ApplicationEvent.LogRequest, + ApplicationEvent.PostLogRequest, + ApplicationEvent.EndRequest, + ApplicationEvent.PreSendRequestHeaders, + ]; + private readonly IHttpResponseEndFeature _previous; private readonly ObjectPool _pool; @@ -22,6 +30,13 @@ public HttpApplicationFeature(HttpContextCore context, IHttpResponseEndFeature p _contextOrApplication = context; _pool = pool; _previous = previousEnd; + + context.Response.OnStarting(static state => + { + var context = (HttpContextCore)state; + + return context.Features.GetRequired().RaiseEventAsync(ApplicationEvent.PreSendRequestHeaders).AsTask(); + }, context); } public RequestNotification CurrentNotification { get; set; } @@ -48,12 +63,12 @@ private HttpApplication InitializeApplication(HttpContextCore context) ValueTask IHttpApplicationFeature.RaiseEventAsync(ApplicationEvent @event) { - RaiseEvent(@event, suppressThrow: false); + RaiseEvent(@event); return ValueTask.CompletedTask; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Must handle all exceptions here")] - private void RaiseEvent(ApplicationEvent appEvent, bool suppressThrow) + private void RaiseEvent(ApplicationEvent appEvent) { (CurrentNotification, IsPostNotification) = appEvent switch { @@ -77,6 +92,7 @@ private void RaiseEvent(ApplicationEvent appEvent, bool suppressThrow) ApplicationEvent.LogRequest => (RequestNotification.LogRequest, false), ApplicationEvent.PostLogRequest => (RequestNotification.LogRequest, true), ApplicationEvent.EndRequest => (RequestNotification.EndRequest, false), + ApplicationEvent.ExecuteRequestHandler => (RequestNotification.ExecuteRequestHandler, false), // Remaining events just continue using the existing notifications _ => (CurrentNotification, IsPostNotification), @@ -91,7 +107,7 @@ private void RaiseEvent(ApplicationEvent appEvent, bool suppressThrow) AddError(ex); Application.InvokeEvent(ApplicationEvent.Error); - if (!suppressThrow) + if (!_suppressThrow.Contains(appEvent)) { ThrowIfErrors(); } @@ -119,10 +135,9 @@ async Task IHttpResponseEndFeature.EndAsync() IsEnded = true; - RaiseEvent(ApplicationEvent.LogRequest, suppressThrow: true); - RaiseEvent(ApplicationEvent.PostLogRequest, suppressThrow: true); - RaiseEvent(ApplicationEvent.EndRequest, suppressThrow: true); - RaiseEvent(ApplicationEvent.PreSendRequestHeaders, suppressThrow: true); + RaiseEvent(ApplicationEvent.LogRequest); + RaiseEvent(ApplicationEvent.PostLogRequest); + RaiseEvent(ApplicationEvent.EndRequest); await _previous.EndAsync(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs index 78ce0fc840..c66a580f7d 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs @@ -114,7 +114,7 @@ public static void UseSystemWebAdapters(this IApplicationBuilder app) } app.UseHttpApplicationEvent( - preEvents: new[] { ApplicationEvent.PreRequestHandlerExecute }, + preEvents: new[] { ApplicationEvent.PreRequestHandlerExecute, ApplicationEvent.ExecuteRequestHandler }, postEvents: new[] { ApplicationEvent.PostRequestHandlerExecute }); } diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/ApplicationEvent.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/ApplicationEvent.cs index a487ea59fc..a48aabe233 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/ApplicationEvent.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/Features/ApplicationEvent.cs @@ -64,6 +64,8 @@ public enum ApplicationEvent SessionEnd, Disposed, + + ExecuteRequestHandler, } #endif diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests.csproj b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests.csproj index c1ad19d9ac..d99fc7db9e 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests.csproj +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs index 0ffa8d2a2b..124d34c55f 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs @@ -3,11 +3,14 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.SystemWebAdapters.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -19,38 +22,42 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests; [Collection(nameof(SelfHostedTests))] public class ModuleTests { - private static readonly string[] Initial = new[] - { - nameof(HttpApplication.BeginRequest), - nameof(HttpApplication.AuthenticateRequest), - nameof(HttpApplication.PostAuthenticateRequest), - nameof(HttpApplication.AuthorizeRequest), - nameof(HttpApplication.PostAuthorizeRequest), - nameof(HttpApplication.ResolveRequestCache), - nameof(HttpApplication.PostResolveRequestCache), - nameof(HttpApplication.MapRequestHandler), - nameof(HttpApplication.PostMapRequestHandler), - nameof(HttpApplication.AcquireRequestState), - nameof(HttpApplication.PostAcquireRequestState), - nameof(HttpApplication.PreRequestHandlerExecute), - nameof(HttpApplication.PostRequestHandlerExecute), - nameof(HttpApplication.ReleaseRequestState), - nameof(HttpApplication.PostReleaseRequestState), - nameof(HttpApplication.UpdateRequestCache), - nameof(HttpApplication.PostUpdateRequestCache), - }; - - private static readonly string[] Always = new[] - { - nameof(HttpApplication.LogRequest), - nameof(HttpApplication.PostLogRequest), - nameof(HttpApplication.EndRequest), - nameof(HttpApplication.PreSendRequestHeaders), - }; + private static readonly ImmutableArray BeforeHandlerEvents = + [ + ApplicationEvent.BeginRequest, + ApplicationEvent.AuthenticateRequest, + ApplicationEvent.PostAuthenticateRequest, + ApplicationEvent.AuthorizeRequest, + ApplicationEvent.PostAuthorizeRequest, + ApplicationEvent.ResolveRequestCache, + ApplicationEvent.PostResolveRequestCache, + ApplicationEvent.MapRequestHandler, + ApplicationEvent.PostMapRequestHandler, + ApplicationEvent.AcquireRequestState, + ApplicationEvent.PostAcquireRequestState, + ApplicationEvent.PreRequestHandlerExecute, + ]; + + private static readonly ImmutableArray AfterHandlerEvents = + [ + ApplicationEvent.PostRequestHandlerExecute, + ApplicationEvent.ReleaseRequestState, + ApplicationEvent.PostReleaseRequestState, + ApplicationEvent.UpdateRequestCache, + ApplicationEvent.PostUpdateRequestCache, + ]; + + private static readonly ImmutableArray EndEvents = + [ + ApplicationEvent.LogRequest, + ApplicationEvent.PostLogRequest, + ApplicationEvent.EndRequest, + ]; public static IEnumerable GetAllEvents() { - var all = Initial.Concat(Always); + IEnumerable all = [.. BeforeHandlerEvents, .. AfterHandlerEvents, .. EndEvents, ApplicationEvent.PreSendRequestHeaders]; + var modes = Enum.GetValues(); foreach (var notification in all) @@ -64,7 +71,7 @@ public static IEnumerable GetAllEvents() [MemberData(nameof(GetAllEvents))] [Theory] - public async Task EndModuleEarly(string notification, RegisterMode mode) + public async Task EndModuleEarly(ApplicationEvent notification, RegisterMode mode) { var expected = GetNotificationsUpTo(notification); var result = await RunAsync(ModuleTestModule.End, notification, mode); @@ -74,7 +81,7 @@ public async Task EndModuleEarly(string notification, RegisterMode mode) [MemberData(nameof(GetAllEvents))] [Theory] - public async Task CompleteModuleEarly(string notification, RegisterMode mode) + public async Task CompleteModuleEarly(ApplicationEvent notification, RegisterMode mode) { var expected = GetNotificationsUpTo(notification); var result = await RunAsync(ModuleTestModule.Complete, notification, mode); @@ -84,46 +91,50 @@ public async Task CompleteModuleEarly(string notification, RegisterMode mode) [MemberData(nameof(GetAllEvents))] [Theory] - public async Task ModulesThrow(string notification, RegisterMode mode) + public async Task ModulesThrow(ApplicationEvent notification, RegisterMode mode) { var expected = GetExpected(notification).ToList(); var result = await RunAsync(ModuleTestModule.Throw, notification, mode); Assert.Equal(expected, result); - static IEnumerable GetExpected(string notification) + static IEnumerable GetExpected(ApplicationEvent notification) { foreach (var item in GetNotificationsUpTo(notification)) { yield return item; - if (string.Equals(item, notification, StringComparison.Ordinal)) + if (item == notification) { - yield return nameof(HttpApplication.Error); + yield return ApplicationEvent.Error; } } } } - private static IEnumerable GetNotificationsUpTo(string notification) + private static IEnumerable GetNotificationsUpTo(ApplicationEvent notification) { - foreach (var n in Initial) + IEnumerable initial = [.. BeforeHandlerEvents, .. AfterHandlerEvents]; + + foreach (var n in initial) { yield return n; - if (string.Equals(n, notification, StringComparison.Ordinal)) + if (n == notification) { break; } } - foreach (var n in Always) + foreach (var n in EndEvents) { yield return n; } + + yield return ApplicationEvent.PreSendRequestHeaders; } - private static async Task> RunAsync(string action, string eventName, RegisterMode mode) + private static async Task> RunAsync(string action, ApplicationEvent @event, RegisterMode mode) { var notifier = new NotificationCollection(); @@ -174,7 +185,7 @@ private static async Task> RunAsync(string action, string eventName }) .StartAsync(); - var url = $"/?action={action}¬ification={eventName}"; + var url = $"/?action={action}¬ification={@event}"; try { @@ -188,7 +199,75 @@ private static async Task> RunAsync(string action, string eventName return notifier; } - private sealed class NotificationCollection : List + [Fact] + public async Task PreSendEventThrownIfNotBuffering() + { + // Arrange + IEnumerable expected = + [ + .. BeforeHandlerEvents, + ApplicationEvent.PreSendRequestHeaders, + .. AfterHandlerEvents, + .. EndEvents + ]; + + var notifier = new NotificationCollection(); + using var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer(options => + { + options.AllowSynchronousIO = true; + }) + .ConfigureServices(services => + { + services.AddRouting(); + services.AddSystemWebAdapters() + .AddHttpApplication(options => + { + options.RegisterModule(); + }); + + }) + .Configure(app => + { + app.Use((ctx, next) => + { + ctx.Features.Set(notifier); + return next(ctx); + }); + app.UseRouting(); + + app.UseSystemWebAdapters(); + + app.UseEndpoints(endpoints => + { + endpoints.Map("/", (HttpContextCore ctx) => + { + ctx.Features.GetRequired().DisableBuffering(); + + var systemWeb = ctx.AsSystemWeb(); + systemWeb.Response.Write(systemWeb.CurrentNotification); + systemWeb.Response.Write(" "); + systemWeb.Response.Output.Flush(); + systemWeb.Response.Write(systemWeb.CurrentNotification); + systemWeb.Response.Output.Flush(); + }); + }); + }); + }) + .StartAsync(); + + // Act + var result = await host.GetTestClient().GetStringAsync(new Uri("/", UriKind.Relative)); + + // Assert + Assert.Equal(expected, notifier); + Assert.Equal(result, $"{ApplicationEvent.ExecuteRequestHandler} {ApplicationEvent.ExecuteRequestHandler}"); + } + + private sealed class NotificationCollection : List { } @@ -223,17 +302,20 @@ public Action Configure(Action next) }; } - private sealed class ModuleTestModule : EventsModule + private sealed class ModulePreSendHeaders : BaseModule { protected override void InvokeEvent(HttpContext context, string name) { - Add(context, name); - base.InvokeEvent(context, name); + context.AsAspNetCore().Features.GetRequired().Add(Enum.Parse(name, ignoreCase: false)); } + } - private static void Add(HttpContextCore context, string name) + private sealed class ModuleTestModule : EventsModule + { + protected override void InvokeEvent(HttpContext context, string name) { - context.Features.GetRequired().Add(name); + context.AsAspNetCore().Features.GetRequired().Add(Enum.Parse(name, ignoreCase: false)); + base.InvokeEvent(context, name); } }