diff --git a/Directory.Build.props b/Directory.Build.props index 5e1f5e6356..87390120b6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ enable - 10 + 11 diff --git a/Microsoft.AspNetCore.SystemWebAdapters.sln b/Microsoft.AspNetCore.SystemWebAdapters.sln index dc3221260c..65b30ecbab 100644 --- a/Microsoft.AspNetCore.SystemWebAdapters.sln +++ b/Microsoft.AspNetCore.SystemWebAdapters.sln @@ -63,7 +63,15 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcApp", "samples\RemoteAut EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcCoreApp", "samples\RemoteAuth\Identity\MvcCoreApp\MvcCoreApp.csproj", "{2BF8EE74-1AB3-4DB8-ADDE-27A35981CA04}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreApp", "samples\CoreApp\CoreApp.csproj", "{431651D7-D40A-403E-813C-496A1414AA22}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreApp", "samples\CoreApp\CoreApp.csproj", "{431651D7-D40A-403E-813C-496A1414AA22}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{4ED7A31C-8DBE-4A32-A17A-D72794F9FE2C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModulesLibrary", "samples\Modules\ModulesLibrary\ModulesLibrary.csproj", "{5597A485-4D9B-4CE6-A489-DEBE9338450F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModulesFramework", "samples\Modules\ModulesFramework\ModulesFramework.csproj", "{B262AD69-11F0-4AE0-949A-AEAA2300C061}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModulesCore", "samples\Modules\ModulesCore\ModulesCore.csproj", "{F8B33C59-27CF-45DC-955C-2EBF9DA9DB7E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -147,6 +155,18 @@ Global {431651D7-D40A-403E-813C-496A1414AA22}.Debug|Any CPU.Build.0 = Debug|Any CPU {431651D7-D40A-403E-813C-496A1414AA22}.Release|Any CPU.ActiveCfg = Release|Any CPU {431651D7-D40A-403E-813C-496A1414AA22}.Release|Any CPU.Build.0 = Release|Any CPU + {5597A485-4D9B-4CE6-A489-DEBE9338450F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5597A485-4D9B-4CE6-A489-DEBE9338450F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5597A485-4D9B-4CE6-A489-DEBE9338450F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5597A485-4D9B-4CE6-A489-DEBE9338450F}.Release|Any CPU.Build.0 = Release|Any CPU + {B262AD69-11F0-4AE0-949A-AEAA2300C061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B262AD69-11F0-4AE0-949A-AEAA2300C061}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B262AD69-11F0-4AE0-949A-AEAA2300C061}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B262AD69-11F0-4AE0-949A-AEAA2300C061}.Release|Any CPU.Build.0 = Release|Any CPU + {F8B33C59-27CF-45DC-955C-2EBF9DA9DB7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8B33C59-27CF-45DC-955C-2EBF9DA9DB7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8B33C59-27CF-45DC-955C-2EBF9DA9DB7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8B33C59-27CF-45DC-955C-2EBF9DA9DB7E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -176,6 +196,10 @@ Global {174A36F1-27ED-43FC-A3A1-00DA58C4E30C} = {9C8EBDB5-FA17-4C9C-8946-04692AC752CE} {2BF8EE74-1AB3-4DB8-ADDE-27A35981CA04} = {9C8EBDB5-FA17-4C9C-8946-04692AC752CE} {431651D7-D40A-403E-813C-496A1414AA22} = {95915611-30BF-4AFF-AE41-5CDC6F57DCF7} + {4ED7A31C-8DBE-4A32-A17A-D72794F9FE2C} = {95915611-30BF-4AFF-AE41-5CDC6F57DCF7} + {5597A485-4D9B-4CE6-A489-DEBE9338450F} = {4ED7A31C-8DBE-4A32-A17A-D72794F9FE2C} + {B262AD69-11F0-4AE0-949A-AEAA2300C061} = {4ED7A31C-8DBE-4A32-A17A-D72794F9FE2C} + {F8B33C59-27CF-45DC-955C-2EBF9DA9DB7E} = {4ED7A31C-8DBE-4A32-A17A-D72794F9FE2C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {DABA3C65-9D74-4EB6-9B1C-730328710EAD} diff --git a/designs/http-modules.md b/designs/http-modules.md new file mode 100644 index 0000000000..30d0d43575 --- /dev/null +++ b/designs/http-modules.md @@ -0,0 +1,109 @@ +# IHttpModules and Emulated Pipeline Support + +> **Note**: This implementation is not tied to IIS and does not hook into any of IIS events if ran on IIS. + +Support for `HttpApplication` and `IHttpModule` is emulated as best as possible on the ASP.NET Core pipeline. This is not tied to IIS and will work on Kestrel or any other host by using middleware to invoke the expected events at the times that best approximate the timing from ASP.NET Core. An attempt has been made to get the events to fire at the appropriate time, but because of the substantial difference between ASP.NET and ASP.NET Core there may still be unexpected behavior. + +In order to register either an `HttpApplication` or `IHttpModule` instance, use the following pattern: + +```csharp +using System.Web; +using ModulesLibrary; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSystemWebAdapters() + // Non-generic version available if no custom HttpApplication is needed + .AddHttpApplication(options => + { + // Size of pool for HttpApplication instances. Should be what the expected concurrent requests will be + options.PoolSize = 10; + + // Register a module by name + options.RegisterModule("Module"); + }); + +var app = builder.Build(); + +app.UseSystemWebAdapters(); + +app.Run(); + +class MyApp : HttpApplication +{ + protected void Application_Start() + { + ... + } + + protected void Session_Start() + { + ... + } + + protected void Begin_Request() + { + ... + } + + ... +} + +class MyModule : IHttpModule +{ + public void Init(HttpApplication app) + { + ... + } + + public void Dispose() + { + } +} +``` + +The normal `.UseSystemWebAdapters()` middleware builder will enable majority of the events. However, the authentication and authorization events require two additional middleware calls in order to enable them if you want the events to fire in the expected order. If they are omitted, they will be called at the point `UseSystemWebAdapters()` is added. + + +```diff +app.UseRouting(); + +app.UseAuthentication(); ++ app.UseAuthenticationEvents(); + +app.UseAuthorization(); ++ app.UseAuthorizationEvents(); + +app.UseSystemWebAdapters(); +``` + +## When should this be used? + +> Most of the time, this should not be used. Prefer direct ASP.NET Core middleware if possible. + +This is intended mostly for scenarios where a module needs to be run on ASP.NET Core but is unable to be migrated easily. Ideally, the code in a module should be restructured to be used as middleware. This is especially recommended when only a single or few events are used; those can usually be migrated in a straightfoward way. + +However, if a module has many thousands of line of code and many events being used (the initial driver of this feature), this can provide a stepping stone to migrating that functionality to ASP.NET Core. + +## Emulated Events + +> For details on how this worked in .NET Framework, see the [official documentation](https://learn.microsoft.com/en-us/dotnet/api/system.web.httpapplication) + +The IIS event pipeline that is expected by `IHttpModule` and `HttpApplication` is emulated using middleware by the adapters. As part of this, it will add additional middleware that will invoke the events. This is done via a feature that is inserted early on in the adapter pipeline [IHttpApplicationFeature](../src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/IHttpApplicationFeature.cs). This exposes the `HttpApplication` for the request, as well as the ability to raise events on it. + +Events have a prescribed order which is replicated with these emulated events. However, because the rest of the ASP.NET Core pipeline is unaware of these events and so some of the state of the request may not be exactly replicated. + +A common pattern is to be able to call `HttpRequest.End()` or `HttpApplication.CompleteRequest()`. Both of these are supported, as well as continuing to raise the events that are raised in IIS with this (including `EndRequest` and the logging events). + +> Note: In the cases in which no modules or `HttpApplication` type is registered, the emulated pipeline is not added to the middleware chain. + +## HttpApplication lifetime + +On ASP.NET Framework, each request would get an individual `HttpApplication` instance. This object contains the following information: + +- Event callbacks registered either on the `HttpApplication` type itself or on registered modules +- Any state contained in the `HttpApplication` instance or its registered modules + +In order to support this, one of the first middlewares invoked will retrieve an instance of `HttpApplication`. This uses a `PooledObjectPolicy` that will create an instance of the application's `HttpApplication` type and register all modules on it. When the request is exiting that middleware, it will return the `HttpApplication` instance to the pool which will also remove the `HttpContext` instance assigned to it. + +This can potentially create a number of instances of `HttpApplication` that are only used a limited number of times. The pool can be controlled by customizing the `HttpApplicationOptions.PoolSize` option or providing a custom implementation of `ObjectPool` that can use the `PooledObjectPolicy` provided to override the pooling behavior. diff --git a/samples/Modules/ModulesCore/ModulesCore.csproj b/samples/Modules/ModulesCore/ModulesCore.csproj new file mode 100644 index 0000000000..3afc9dcb97 --- /dev/null +++ b/samples/Modules/ModulesCore/ModulesCore.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/samples/Modules/ModulesCore/Program.cs b/samples/Modules/ModulesCore/Program.cs new file mode 100644 index 0000000000..1d8cdc70ac --- /dev/null +++ b/samples/Modules/ModulesCore/Program.cs @@ -0,0 +1,27 @@ +using System.Web; +using ModulesLibrary; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSystemWebAdapters() + .AddHttpApplication(options => + { + // Size of pool for HttpApplication instances. Should be what the expected concurrent requests will be + options.PoolSize = 10; + + // Register a module by name + options.RegisterModule("Events"); + }); + +var app = builder.Build(); + +app.UseSystemWebAdapters(); + +app.Run(); + +class MyApp : HttpApplication +{ + protected void Application_Start() + { + } +} diff --git a/samples/Modules/ModulesCore/Properties/launchSettings.json b/samples/Modules/ModulesCore/Properties/launchSettings.json new file mode 100644 index 0000000000..22b1c48d23 --- /dev/null +++ b/samples/Modules/ModulesCore/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:30447", + "sslPort": 44395 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5202", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7148;http://localhost:5202", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/Modules/ModulesCore/appsettings.Development.json b/samples/Modules/ModulesCore/appsettings.Development.json new file mode 100644 index 0000000000..770d3e9314 --- /dev/null +++ b/samples/Modules/ModulesCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/Modules/ModulesCore/appsettings.json b/samples/Modules/ModulesCore/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/samples/Modules/ModulesCore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/Modules/ModulesCore/wwwroot/favicon.ico b/samples/Modules/ModulesCore/wwwroot/favicon.ico new file mode 100644 index 0000000000..63e859b476 Binary files /dev/null and b/samples/Modules/ModulesCore/wwwroot/favicon.ico differ diff --git a/samples/Modules/ModulesFramework/Handler.cs b/samples/Modules/ModulesFramework/Handler.cs new file mode 100644 index 0000000000..73d6a41fb2 --- /dev/null +++ b/samples/Modules/ModulesFramework/Handler.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace ModulesFramework +{ + public class Handler : IHttpHandler + { + public bool IsReusable => true; + + public void ProcessRequest(HttpContext context) + { + } + } +} diff --git a/samples/Modules/ModulesFramework/ModulesFramework.csproj b/samples/Modules/ModulesFramework/ModulesFramework.csproj new file mode 100644 index 0000000000..b7230fcfa2 --- /dev/null +++ b/samples/Modules/ModulesFramework/ModulesFramework.csproj @@ -0,0 +1,141 @@ + + + + + Debug + AnyCPU + + + 2.0 + {B262AD69-11F0-4AE0-949A-AEAA2300C061} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + ModulesFramework + ModulesFramework + v4.7.2 + true + + 44396 + + + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + true + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + ..\..\..\packages\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.2.0.1\lib\net45\Microsoft.CodeDom.Providers.DotNetCompilerPlatform.dll + + + + + + + + + + + + + Web.config + + + Web.config + + + + + {632e6195-4304-4c67-aabb-7cfc3f9086b6} + Microsoft.AspNetCore.SystemWebAdapters.Abstractions + + + {6931fefb-dc18-4b3f-8afc-eda03063a518} + Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices + + + {55c1bbe0-b922-46b0-8f2c-8472bc9a5f33} + Microsoft.AspNetCore.SystemWebAdapters + + + {06f6ad9d-b54d-4659-b7b8-4eef37474c5b} + ModulesLibrary + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + + + + + True + True + 62594 + / + https://localhost:44396/ + False + False + + + False + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/samples/Modules/ModulesFramework/Properties/AssemblyInfo.cs b/samples/Modules/ModulesFramework/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..b951bac601 --- /dev/null +++ b/samples/Modules/ModulesFramework/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ModulesFramework")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("ModulesFramework")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b262ad69-11f0-4ae0-949a-aeaa2300c061")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/Modules/ModulesFramework/Web.Debug.config b/samples/Modules/ModulesFramework/Web.Debug.config new file mode 100644 index 0000000000..fae9cfefa9 --- /dev/null +++ b/samples/Modules/ModulesFramework/Web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/Modules/ModulesFramework/Web.Release.config b/samples/Modules/ModulesFramework/Web.Release.config new file mode 100644 index 0000000000..da6e960b8d --- /dev/null +++ b/samples/Modules/ModulesFramework/Web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/Modules/ModulesFramework/Web.config b/samples/Modules/ModulesFramework/Web.config new file mode 100644 index 0000000000..f0833cf38f --- /dev/null +++ b/samples/Modules/ModulesFramework/Web.config @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/Modules/ModulesFramework/packages.config b/samples/Modules/ModulesFramework/packages.config new file mode 100644 index 0000000000..55d586f298 --- /dev/null +++ b/samples/Modules/ModulesFramework/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/Modules/ModulesLibrary/BaseModule.cs b/samples/Modules/ModulesLibrary/BaseModule.cs new file mode 100644 index 0000000000..3781d680de --- /dev/null +++ b/samples/Modules/ModulesLibrary/BaseModule.cs @@ -0,0 +1,56 @@ +using System; +using System.Web; + +#nullable enable + +namespace ModulesLibrary +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper", Justification = "Source shared with .NET Framework that does not have the method")] + public abstract class BaseModule : IHttpModule + { + public void Dispose() + { + } + + public void Init(HttpApplication application) + { + if (application is null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.AcquireRequestState += (s, e) => WriteDetails(s, nameof(application.AcquireRequestState)); + application.AuthenticateRequest += (s, e) => WriteDetails(s, nameof(application.AuthenticateRequest)); + application.AuthorizeRequest += (s, e) => WriteDetails(s, nameof(application.AuthorizeRequest)); + application.BeginRequest += (s, e) => WriteDetails(s, nameof(application.BeginRequest)); + application.EndRequest += (s, e) => WriteDetails(s, nameof(application.EndRequest)); + application.Error += (s, e) => WriteDetails(s, nameof(application.Error)); + application.LogRequest += (s, e) => WriteDetails(s, nameof(application.LogRequest)); + application.MapRequestHandler += (s, e) => WriteDetails(s, nameof(application.MapRequestHandler)); + application.PostAcquireRequestState += (s, e) => WriteDetails(s, nameof(application.PostAcquireRequestState)); + application.PostAuthenticateRequest += (s, e) => WriteDetails(s, nameof(application.PostAuthenticateRequest)); + application.PostAuthorizeRequest += (s, e) => WriteDetails(s, nameof(application.PostAuthorizeRequest)); + application.PostLogRequest += (s, e) => WriteDetails(s, nameof(application.PostLogRequest)); + application.PostMapRequestHandler += (s, e) => WriteDetails(s, nameof(application.PostMapRequestHandler)); + application.PostReleaseRequestState += (s, e) => WriteDetails(s, nameof(application.PostReleaseRequestState)); + application.PostRequestHandlerExecute += (s, e) => WriteDetails(s, nameof(application.PostRequestHandlerExecute)); + application.PostResolveRequestCache += (s, e) => WriteDetails(s, nameof(application.PostResolveRequestCache)); + application.PostUpdateRequestCache += (s, e) => WriteDetails(s, nameof(application.PostUpdateRequestCache)); + application.PreRequestHandlerExecute += (s, e) => WriteDetails(s, nameof(application.PreRequestHandlerExecute)); + application.PreSendRequestHeaders += (s, e) => WriteDetails(s, nameof(application.PreSendRequestHeaders)); + application.ReleaseRequestState += (s, e) => WriteDetails(s, nameof(application.ReleaseRequestState)); + application.ResolveRequestCache += (s, e) => WriteDetails(s, nameof(application.ResolveRequestCache)); + application.UpdateRequestCache += (s, e) => WriteDetails(s, nameof(application.UpdateRequestCache)); + } + + private void WriteDetails(object? sender, string name) + { + if (sender is HttpApplication { Context: { } context }) + { + InvokeEvent(context, name); + } + } + + protected abstract void InvokeEvent(HttpContext context, string name); + } +} diff --git a/samples/Modules/ModulesLibrary/EventsModule.cs b/samples/Modules/ModulesLibrary/EventsModule.cs new file mode 100644 index 0000000000..a584ed5adf --- /dev/null +++ b/samples/Modules/ModulesLibrary/EventsModule.cs @@ -0,0 +1,40 @@ +using System; +using System.Web; + +namespace ModulesLibrary +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper", Justification = "Source shared with .NET Framework that does not have the method")] + public class EventsModule : BaseModule + { + public const string End = "end"; + public const string Complete = "complete"; + public const string Throw = "throw"; + + protected override void InvokeEvent(HttpContext context, string name) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Response.ContentType = "text/plain"; + + context.Response.Output.WriteLine(name); + + if (string.Equals(name, context.Request.QueryString["notification"], StringComparison.OrdinalIgnoreCase)) + { + switch (context.Request.QueryString["action"]) + { + case End: + context.Response.End(); + break; + case Complete: + context.ApplicationInstance.CompleteRequest(); + break; + case Throw: + throw new InvalidOperationException(); + } + } + } + } +} diff --git a/samples/Modules/ModulesLibrary/ModulesLibrary.csproj b/samples/Modules/ModulesLibrary/ModulesLibrary.csproj new file mode 100644 index 0000000000..a0604e292f --- /dev/null +++ b/samples/Modules/ModulesLibrary/ModulesLibrary.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0;net6.0 + 11 + + + + + + + diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationExtensions.cs new file mode 100644 index 0000000000..9c50771a6c --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationExtensions.cs @@ -0,0 +1,198 @@ +// 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.Web; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.SystemWebAdapters; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class HttpApplicationExtensions +{ + public static ISystemWebAdapterBuilder AddHttpApplication(this ISystemWebAdapterBuilder builder, Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.TryAddSingleton(); + builder.Services.AddTransient(); + builder.Services.TryAddSingleton>(ctx => ctx.GetRequiredService()); + builder.Services.TryAddSingleton>(sp => + { + var options = sp.GetRequiredService>(); + var policy = sp.GetRequiredService>(); + + var provider = new DefaultObjectPoolProvider + { + MaximumRetained = options.Value.PoolSize, + }; + + return provider.Create(policy); + }); + + builder.Services.AddOptions() + .Configure(configure) + .PostConfigure(c => c.MakeReadOnly()); + + return builder; + } + + public static ISystemWebAdapterBuilder AddHttpApplication(this ISystemWebAdapterBuilder builder, Action configure) + where TApp : HttpApplication + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configure); + + builder.AddHttpApplication(options => + { + options.ApplicationType = typeof(TApp); + + configure(options); + }); + + 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()); + + internal static void UseHttpApplicationEvent(this IApplicationBuilder app, ApplicationEvent[] preEvents, ApplicationEvent[] postEvents) + { + if (app.AreHttpApplicationEventsRequired()) + { + app.Use(async (ctx, next) => + { + var appFeature = ctx.Features.GetRequired(); + var endFeature = ctx.Features.GetRequired(); + + foreach (var @event in preEvents) + { + await appFeature.RaiseEventAsync(@event); + + if (endFeature.IsEnded) + { + return; + } + } + + await next(ctx); + + foreach (var @event in postEvents) + { + if (endFeature.IsEnded) + { + return; + } + + await appFeature.RaiseEventAsync(@event); + } + }); + } + } + + public static IApplicationBuilder UseAuthenticationEvents(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + if (app.HasBeenAdded()) + { + return app; + } + + app.UseSystemWebAdapterFeatures(); + app.UseHttpApplicationEvent(ApplicationEvent.AuthenticateRequest, ApplicationEvent.PostAuthenticateRequest); + + return app; + } + + public static IApplicationBuilder UseAuthorizationEvents(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + if (app.HasBeenAdded()) + { + return app; + } + + app.UseSystemWebAdapterFeatures(); + app.UseAuthenticationEvents(); + app.UseHttpApplicationEvent(ApplicationEvent.AuthorizeRequest, ApplicationEvent.PostAuthorizeRequest); + + return app; + } + + internal static bool AreHttpApplicationEventsRequired(this IApplicationBuilder builder) + { + const string AreHttpApplicationEventsRequired = nameof(AreHttpApplicationEventsRequired); + + if (builder.Properties.TryGetValue(AreHttpApplicationEventsRequired, out var existing) && existing is bool b) + { + return b; + } + + var options = builder.ApplicationServices.GetRequiredService>().Value; + + var hasModules = options.Modules.Count > 0; + var hasCustomApplication = options.ApplicationType != typeof(HttpApplication); + + var areEventsRequired = hasModules || hasCustomApplication; + + builder.Properties[AreHttpApplicationEventsRequired] = areEventsRequired; + + return areEventsRequired; + } + + private sealed class HttpApplicationStartupFilter : IStartupFilter + { + private readonly ObjectPool _pool; + + public HttpApplicationStartupFilter(ObjectPool pool) + { + _pool = pool; + } + + public Action Configure(Action next) => builder => + { + CallStartup(builder.ApplicationServices); + next(builder); + }; + + private void CallStartup(IServiceProvider services) + { + using var scope = services.CreateScope(); + + var app = _pool.Get(); + + // ASP.NET Framework provided an HttpContext instance that was not tied to a request for Start + app.Context = new DefaultHttpContext + { + RequestServices = scope.ServiceProvider, + }; + + try + { + // This is only invoked at the beginning of the application + // See https://referencesource.microsoft.com/#System.Web/HttpApplication.cs,2417 + app.InvokeEvent(ApplicationEvent.ApplicationStart); + } + finally + { + _pool.Return(app); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationMiddleware.cs new file mode 100644 index 0000000000..ff51752ec5 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationMiddleware.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.AspNetCore.SystemWebAdapters; + +internal class HttpApplicationMiddleware +{ + private readonly RequestDelegate _next; + private readonly ObjectPool _pool; + + public HttpApplicationMiddleware(RequestDelegate next, ObjectPool pool) + { + _next = next; + _pool = pool; + } + + public async Task InvokeAsync(HttpContextCore context) + { + var app = _pool.Get(); + + var endFeature = context.Features.GetRequired(); + var httpApplicationFeature = new RequestHttpApplicationFeature(app, endFeature); + + context.Features.Set(httpApplicationFeature); + context.Features.Set(httpApplicationFeature); + + try + { + app.Context = context; + + context.Features.GetRequired().EnableBuffering(BufferResponseStreamAttribute.DefaultMemoryThreshold, default); + + try + { + await _next(context); + } + finally + { + await context.Features.GetRequired().EndAsync(); + } + } + finally + { + context.Features.Set(endFeature); + context.Features.Set(null); + + _pool.Return(app); + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationOptions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationOptions.cs new file mode 100644 index 0000000000..b81947a033 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationOptions.cs @@ -0,0 +1,152 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Web; + +using static System.FormattableString; + +namespace Microsoft.AspNetCore.SystemWebAdapters; + +public class HttpApplicationOptions +{ + private readonly ModuleCollection _modules = new(); + + private Type _applicationType = typeof(HttpApplication); + + public Type ApplicationType + { + get => _applicationType; + set + { + ArgumentNullException.ThrowIfNull(value); + + _modules.CheckIsReadOnly(); + + if (!_applicationType.IsAssignableTo(typeof(HttpApplication))) + { + throw new InvalidOperationException($"Type {value.FullName} is not a valid HttpApplication"); + } + + _applicationType = value; + } + } + + public IDictionary Modules => _modules; + + internal void MakeReadOnly() => _modules.MakeReadOnly(); + + /// + /// Gets or sets the number of retained for reuse. In order to support modules and appplications that may contain state, + /// a unique instance is required for each request. This type should be set to the average number of concurrent requests expected to be seen. + /// + public int PoolSize { get; set; } = 100; + + public void RegisterModule(string? name = null) + where T : IHttpModule + => RegisterModule(typeof(T), name); + + public void RegisterModule(Type type, string? name = null) + { + ArgumentNullException.ThrowIfNull(type); + + Modules.Add(name ?? MakeUniqueModuleName(type), type); + + // Gets a dynamic name similar to how ASP.NET Framework did in the static HttpApplication.RegisterModule(Type moduleType) method + static string MakeUniqueModuleName(Type type) + => Invariant($"__DynamicModule_{type.AssemblyQualifiedName}_{Guid.NewGuid()}"); + } + + /// + /// A collection that validates that the types added are actual IHttpModule types + /// + private sealed class ModuleCollection : IDictionary + { + private readonly Dictionary _inner; + + public ModuleCollection() + { + _inner = new(StringComparer.InvariantCultureIgnoreCase); + } + + public Type this[string key] + { + get => _inner[key]; + set => _inner[key] = value; + } + + public ICollection Keys => _inner.Keys; + + public ICollection Values => _inner.Values; + + public int Count => _inner.Count; + + public bool IsReadOnly { get; private set; } + + public void Add(string key, Type type) + { + CheckIsReadOnly(); + + if (Contains(new(key, type))) + { + throw new InvalidOperationException($"Module {type.FullName} is already registered with key '{key}'."); + } + + if (!type.IsAssignableTo(typeof(IHttpModule))) + { + throw new InvalidOperationException($"Type {type.FullName} is not a valid IHttpModule."); + } + + _inner.Add(key, type); + } + + public void Add(KeyValuePair item) => Add(item.Key, item.Value); + + public void Clear() + { + CheckIsReadOnly(); + _inner.Clear(); + } + + public bool Contains(KeyValuePair item) => ((IDictionary)_inner).Contains(item); + + public bool ContainsKey(string key) => _inner.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((ICollection>)_inner).CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() => ((IEnumerable>)_inner).GetEnumerator(); + + public void MakeReadOnly() + { + IsReadOnly = true; + } + + public bool Remove(string key) + { + CheckIsReadOnly(); + return _inner.Remove(key); + } + + public bool Remove(KeyValuePair item) + { + CheckIsReadOnly(); + return ((ICollection>)_inner).Remove(item); + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out Type value) => _inner.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + + public void CheckIsReadOnly() + { + if (IsReadOnly) + { + throw new InvalidOperationException("Module collection is readonly"); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationPooledObjectPolicy.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationPooledObjectPolicy.cs new file mode 100644 index 0000000000..ab9ee9e048 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/HttpApplicationPooledObjectPolicy.cs @@ -0,0 +1,244 @@ +// 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.Linq; +using System.Reflection; +using System.Web; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.SystemWebAdapters; + +/// +/// A policy to create an HttpApplication, associated modules, and add intrinsic events. For details, see the official documentation for +/// how this worked in ASP.NET Framework: https://docs.microsoft.com/en-us/dotnet/api/system.web.httpapplication#remarks +/// +internal sealed partial class HttpApplicationPooledObjectPolicy : PooledObjectPolicy, IDisposable +{ + private static readonly HashSet UnsupportedEvents = new(StringComparer.OrdinalIgnoreCase) + { + // Fired before the ASP.NET page framework sends content to a requesting client (browser). + "Application_PreSendContent", + + // Fired when the last instance of an HttpApplication class is destroyed. It's fired only once during an application's lifetime. + "Application_End", + }; + + private static readonly Dictionary> KnownEvents = new(StringComparer.OrdinalIgnoreCase) + { + // Fired when the first instance of the HttpApplication class is created. It allows you to create objects that are accessible by all HttpApplication instances. + { "Application_Start", (app, handler) => app.ApplicationStart += handler }, + + // Fired when an application initializes or is first called. It's invoked for all HttpApplication object instances. + { "Application_Init", (app, handler) => app.ApplicationInit += handler }, + + // Fired just before an application is destroyed. This is the ideal location for cleaning up previously used resources. + { "Application_Disposed", (app, handler) => app.Disposed += handler }, + + // Fired when an unhandled exception is encountered within the application. + { "Application_Error", (app, handler) => app.Error += handler }, + + // Fired when an application request is received. It's the first event fired for a request, which is often a page request (URL) that a user enters. + { "Application_BeginRequest", (app, handler) => app.BeginRequest += handler }, + + // The last event fired for an application request. + { "Application_EndRequest", (app, handler) => app.EndRequest += handler }, + + // Fired before the ASP.NET page framework begins executing an event handler like a page or Web service. + { "Application_PreRequestHandlerExecute", (app, handler) => app.PreRequestHandlerExecute += handler }, + + // Fired when the ASP.NET page framework is finished executing an event handler. + { "Application_PostRequestHandlerExecute", (app, handler) => app.PostRequestHandlerExecute += handler }, + + // Fired before the ASP.NET page framework sends HTTP headers to a requesting client (browser). + { "Application_PreSendRequestHeaders", (app, handler) => app.PreSendRequestHeaders += handler }, + + + // Fired when the ASP.NET page framework gets the current state (Session state) related to the current request. + { "Application_AcquireRequestState", (app, handler) => app.AcquireRequestState += handler }, + + // Fired when the ASP.NET page framework completes execution of all event handlers. This results in all state modules to save their current state data. + { "Application_ReleaseRequestState", (app, handler) => app.ReleaseRequestState += handler }, + + // Fired when the ASP.NET page framework completes an authorization request. It allows caching modules to serve the request from the cache, thus bypassing handler execution. + { "Application_ResolveRequestCache", (app, handler) => app.ResolveRequestCache += handler }, + + // Fired when the ASP.NET page framework completes handler execution to allow caching modules to store responses to be used to handle subsequent requests. + { "Application_UpdateRequestCache", (app, handler) => app.UpdateRequestCache += handler }, + + // Fired when the security module has established the current user's identity as valid. At this point, the user's credentials have been validated. + { "Application_AuthenticateRequest", (app, handler) => app.AuthenticateRequest += handler }, + + // Fired when the security module has verified that a user can access resources. + { "Application_AuthorizeRequest", (app, handler) => app.AuthorizeRequest += handler }, + + // Fired when a new user visits the application Web site. + { "Session_Start", (app, handler) => app.SessionStart += handler }, + + // Fired when a user's session times out, ends, or they leave the application Web site. + { "Session_End", (app, handler) => app.SessionEnd += handler }, + }; + + [LoggerMessage(0, LogLevel.Information, "Registered event {ApplicationType}.{EventName}")] + private partial void LogRegistration(string applicationType, string eventName); + + [LoggerMessage(1, LogLevel.Warning, "HttpApplication event {ApplicationType}.{EventName} is unsupported")] + private partial void LogUnsupported(string applicationType, string eventName); + + [LoggerMessage(2, LogLevel.Warning, "{ApplicationType}.{EventName} has unsupported signature")] + private partial void LogInvalid(string applicationType, string eventName); + + private readonly ILogger _logger; + private readonly IServiceProvider _services; + private readonly Lazy> _factory; + private readonly HttpApplicationState _state; + + public HttpApplicationPooledObjectPolicy(IServiceProvider services, IOptions options, ILogger logger) + { + _logger = logger; + _services = services; + _state = new HttpApplicationState(); + _factory = new Lazy>(() => CreateFactory(options.Value), isThreadSafe: true); + } + + public override HttpApplication Create() + { + var app = _factory.Value(_services); + + // This is invoked each time an HttpApplication is constructed + app.InvokeEvent(ApplicationEvent.ApplicationInit); + + return app; + } + + public override bool Return(HttpApplication obj) + { + obj.Context = null!; + return true; + } + + /// + /// Creates a callback that will regsiter implicit events on . + /// + /// Options for the . + /// A callback to create a new instance. + /// + private Func CreateFactory(HttpApplicationOptions options) + { + var eventInitializer = GetEventInitializer(options); + var factory = ActivatorUtilities.CreateFactory(options.ApplicationType, Array.Empty()); + var moduleFactories = options.Modules + .Select(m => (m.Key, ActivatorUtilities.CreateFactory(m.Value, Array.Empty()))) + .ToList(); + + if (moduleFactories.Count == 0) + { + return sp => + { + var app = (HttpApplication)factory(sp, null); + app.Initialize(HttpModuleCollection.Empty, _state, eventInitializer); + return app; + }; + } + + return sp => + { + var app = (HttpApplication)factory(sp, null); + var modules = new (string, IHttpModule)[moduleFactories.Count]; + + for (var i = 0; i < moduleFactories.Count; i++) + { + var module = (IHttpModule)moduleFactories[i].Item2(sp, null); + modules[i] = (moduleFactories[i].Key, module); + } + + app.Initialize(new(modules), _state, eventInitializer); + + return app; + }; + } + + private Action GetEventInitializer(HttpApplicationOptions options) + { + var type = options.ApplicationType; + var typeName = type.FullName ?? type.Name; + Action? known = default; + + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var state = EventParseState.None; + + if (UnsupportedEvents.Contains(method.Name)) + { + state = EventParseState.NotSupported; + } + else if (KnownEvents.TryGetValue(method.Name, out var registration) && CreateHandler(method, ref state) is { } handler) + { + known += app => registration(app, handler(app)); + } + + if (state is EventParseState.Registered) + { + LogRegistration(typeName, method.Name); + } + else if (state is EventParseState.NotSupported) + { + LogUnsupported(typeName, method.Name); + } + else if (state is EventParseState.InvalidSignature) + { + LogInvalid(typeName, method.Name); + } + } + + return known ?? (_ => { }); + } + + private static BindableEventHandler? CreateHandler(MethodInfo method, ref EventParseState state) + { + var parameters = method.GetParameters(); + + if (method.ReturnType == typeof(void)) + { + if (parameters.Length == 0) + { + state = EventParseState.Registered; + + return app => + { + var d = method.CreateDelegate(app); + + return (s, e) => d(); + }; + } + + if (parameters.Length == 2 && parameters[0].ParameterType == typeof(object) && parameters[1].ParameterType == typeof(EventArgs)) + { + state = EventParseState.Registered; + return app => method.CreateDelegate(app); + } + } + + state = EventParseState.InvalidSignature; + return null; + } + + public void Dispose() + { + _state.Dispose(); + } + + private delegate EventHandler BindableEventHandler(HttpApplication app); + + private enum EventParseState + { + None, + Registered, + NotSupported, + InvalidSignature, + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/RequestHttpApplicationFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/RequestHttpApplicationFeature.cs new file mode 100644 index 0000000000..7e8924b56b --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpApplication/RequestHttpApplicationFeature.cs @@ -0,0 +1,114 @@ +// 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.Threading.Tasks; +using System.Web; + +namespace Microsoft.AspNetCore.SystemWebAdapters; + +internal sealed class RequestHttpApplicationFeature : IHttpApplicationFeature, IHttpResponseEndFeature +{ + private readonly IHttpResponseEndFeature _previous; + private List? _exceptions; + + public RequestHttpApplicationFeature(HttpApplication app, IHttpResponseEndFeature previousEnd) + { + Application = app; + _previous = previousEnd; + } + + public RequestNotification CurrentNotification { get; set; } + + public bool IsPostNotification { get; set; } + + public bool IsEnded { get; private set; } + + public HttpApplication Application { get; } + + ValueTask IHttpApplicationFeature.RaiseEventAsync(ApplicationEvent @event) + { + RaiseEvent(@event, suppressThrow: false); + return ValueTask.CompletedTask; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Need to handle all exceptions")] + private void RaiseEvent(ApplicationEvent appEvent, bool suppressThrow) + { + (CurrentNotification, IsPostNotification) = appEvent switch + { + ApplicationEvent.BeginRequest => (RequestNotification.BeginRequest, false), + ApplicationEvent.AuthenticateRequest => (RequestNotification.AuthenticateRequest, false), + ApplicationEvent.PostAuthenticateRequest => (RequestNotification.AuthenticateRequest, true), + ApplicationEvent.AuthorizeRequest => (RequestNotification.AuthorizeRequest, false), + ApplicationEvent.PostAuthorizeRequest => (RequestNotification.AuthorizeRequest, true), + ApplicationEvent.ResolveRequestCache => (RequestNotification.ResolveRequestCache, false), + ApplicationEvent.PostResolveRequestCache => (RequestNotification.ResolveRequestCache, true), + ApplicationEvent.MapRequestHandler => (RequestNotification.MapRequestHandler, false), + ApplicationEvent.PostMapRequestHandler => (RequestNotification.MapRequestHandler, true), + ApplicationEvent.AcquireRequestState => (RequestNotification.AcquireRequestState, false), + ApplicationEvent.PostAcquireRequestState => (RequestNotification.AcquireRequestState, true), + ApplicationEvent.PreRequestHandlerExecute => (RequestNotification.PreExecuteRequestHandler, false), + ApplicationEvent.PostRequestHandlerExecute => (RequestNotification.ExecuteRequestHandler, true), + ApplicationEvent.ReleaseRequestState => (RequestNotification.ReleaseRequestState, false), + ApplicationEvent.PostReleaseRequestState => (RequestNotification.ReleaseRequestState, true), + ApplicationEvent.UpdateRequestCache => (RequestNotification.UpdateRequestCache, false), + ApplicationEvent.PostUpdateRequestCache => (RequestNotification.UpdateRequestCache, true), + ApplicationEvent.LogRequest => (RequestNotification.LogRequest, false), + ApplicationEvent.PostLogRequest => (RequestNotification.LogRequest, true), + ApplicationEvent.EndRequest => (RequestNotification.EndRequest, false), + + // Remaining events just continue using the existing notifications + _ => (CurrentNotification, IsPostNotification), + }; + + try + { + Application.InvokeEvent(appEvent); + } + catch (Exception ex) + { + AddError(ex); + Application.InvokeEvent(ApplicationEvent.Error); + + if (!suppressThrow) + { + ThrowIfErrors(); + } + } + } + + private void ThrowIfErrors() + { + if (_exceptions is [{ } exception]) + { + throw exception; + } + else if (_exceptions is { } exceptions) + { + throw new AggregateException(exceptions); + } + } + + async Task IHttpResponseEndFeature.EndAsync() + { + if (IsEnded) + { + return; + } + + IsEnded = true; + + RaiseEvent(ApplicationEvent.LogRequest, suppressThrow: true); + RaiseEvent(ApplicationEvent.PostLogRequest, suppressThrow: true); + RaiseEvent(ApplicationEvent.EndRequest, suppressThrow: true); + RaiseEvent(ApplicationEvent.PreSendRequestHeaders, suppressThrow: true); + + await _previous.EndAsync(); + + ThrowIfErrors(); + } + + private void AddError(Exception ex) => (_exceptions ??= new()).Add(ex); +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionEventsMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionEventsMiddleware.cs new file mode 100644 index 0000000000..bcddf1a955 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionEventsMiddleware.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.SystemWebAdapters; + +internal sealed class SessionEventsMiddleware +{ + private readonly RequestDelegate _next; + + public SessionEventsMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContextCore context) + { + var app = context.Features.GetRequired(); + var session = context.GetAdapter().Session; + + if (session is { IsNewSession: true }) + { + await app.RaiseEventAsync(ApplicationEvent.SessionEnd); + } + + await _next(context); + + if (session is { State.IsAbandoned: true }) + { + await app.RaiseEventAsync(ApplicationEvent.SessionEnd); + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionMiddleware.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionMiddleware.cs index c9444bec3c..cd6b027c72 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionMiddleware.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SessionMiddleware.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.SystemWebAdapters; - internal partial class SessionMiddleware { private readonly RequestDelegate _next; diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs index dfbb4d217a..9b0ac5ab46 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/SystemWebAdaptersExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Runtime.CompilerServices; using System.Web; using System.Web.Caching; using System.Web.Configuration; @@ -25,19 +26,73 @@ public static ISystemWebAdapterBuilder AddSystemWebAdapters(this IServiceCollect .AddMvc(); } - public static void UseSystemWebAdapters(this IApplicationBuilder app) + internal static bool HasBeenAdded(this IApplicationBuilder app, [CallerMemberName] string key = null!) { - ArgumentNullException.ThrowIfNull(app); + if (app.Properties.ContainsKey(key)) + { + return true; + } - HttpRuntime.Current = app.ApplicationServices.GetRequiredService(); + app.Properties[key] = true; + return false; + } + + internal static void UseSystemWebAdapterFeatures(this IApplicationBuilder app) + { + if (app.HasBeenAdded()) + { + return; + } app.UseMiddleware(); - app.UseMiddleware(); app.UseMiddleware(); - app.UseMiddleware(); app.UseMiddleware(); + + app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); + + app.UseHttpApplication(); + } + + public static void UseSystemWebAdapters(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app); + + HttpRuntime.Current = app.ApplicationServices.GetRequiredService(); + + app.UseSystemWebAdapterFeatures(); + app.UseAuthenticationEvents(); + app.UseAuthorizationEvents(); + + app.UseHttpApplicationEvent( + preEvents: new[] + { + ApplicationEvent.ResolveRequestCache, + ApplicationEvent.PostResolveRequestCache, + ApplicationEvent.MapRequestHandler, + ApplicationEvent.PostMapRequestHandler, + ApplicationEvent.AcquireRequestState, + ApplicationEvent.PostAcquireRequestState, + }, + postEvents: new[] + { + ApplicationEvent.ReleaseRequestState, + ApplicationEvent.PostReleaseRequestState, + ApplicationEvent.UpdateRequestCache, + ApplicationEvent.PostUpdateRequestCache, + }); + + app.UseMiddleware(); + + if (app.AreHttpApplicationEventsRequired()) + { + app.UseMiddleware(); + } + + app.UseHttpApplicationEvent( + preEvents: new[] { ApplicationEvent.PreRequestHandlerExecute }, + postEvents: new[] { ApplicationEvent.PostRequestHandlerExecute }); } /// diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/ApplicationEvent.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/ApplicationEvent.cs new file mode 100644 index 0000000000..aa38ff2dd6 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/ApplicationEvent.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP + +namespace Microsoft.AspNetCore.SystemWebAdapters; + +internal enum ApplicationEvent +{ + ApplicationStart, + + ApplicationInit, + + BeginRequest, + + AuthenticateRequest, + + PostAuthenticateRequest, + + AuthorizeRequest, + + PostAuthorizeRequest, + + ResolveRequestCache, + + PostResolveRequestCache, + + MapRequestHandler, + + PostMapRequestHandler, + + AcquireRequestState, + + PostAcquireRequestState, + + PreRequestHandlerExecute, + + PostRequestHandlerExecute, + + ReleaseRequestState, + + PostReleaseRequestState, + + UpdateRequestCache, + + PostUpdateRequestCache, + + LogRequest, + + PostLogRequest, + + EndRequest, + + PreSendRequestHeaders, + + Error, + + SessionStart, + + SessionEnd, + + Disposed, +} + +#endif diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/IHttpApplicationFeature.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/IHttpApplicationFeature.cs new file mode 100644 index 0000000000..895e3b0674 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/IHttpApplicationFeature.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NETCOREAPP + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace Microsoft.AspNetCore.SystemWebAdapters; + +internal interface IHttpApplicationFeature +{ + /// + /// Gets the that is assigned to the current request. + /// + HttpApplication Application { get; } + + /// + /// Raises events for the current application assigned to the request. See https://docs.microsoft.com/en-us/dotnet/api/system.web.httpapplication#remarks for details on how this worked in .NET Framework. + /// + /// + /// + ValueTask RaiseEventAsync(ApplicationEvent @event); + + /// + /// Gets the current of where the request is in an emulated IIS pipeline. + /// + RequestNotification CurrentNotification { get; } + + /// + /// Gets whether the of the emulated IIS pipeline is in a post condition. + /// + bool IsPostNotification { get; } +} + +#endif diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs index 035b91a0c5..94c11b434a 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs @@ -20,6 +20,62 @@ namespace System.Web { + public partial class HttpApplication : System.IDisposable + { + public HttpApplication() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public System.Web.HttpApplicationState Application { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.HttpContext Context { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.HttpModuleCollection Modules { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.HttpRequest Request { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.HttpResponse Response { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.HttpServerUtility Server { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.SessionState.HttpSessionState Session { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Security.Principal.IPrincipal User { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public event System.EventHandler AcquireRequestState { add { } remove { } } + public event System.EventHandler AuthenticateRequest { add { } remove { } } + public event System.EventHandler AuthorizeRequest { add { } remove { } } + public event System.EventHandler BeginRequest { add { } remove { } } + public event System.EventHandler Disposed { add { } remove { } } + public event System.EventHandler EndRequest { add { } remove { } } + public event System.EventHandler Error { add { } remove { } } + public event System.EventHandler LogRequest { add { } remove { } } + public event System.EventHandler MapRequestHandler { add { } remove { } } + public event System.EventHandler PostAcquireRequestState { add { } remove { } } + public event System.EventHandler PostAuthenticateRequest { add { } remove { } } + public event System.EventHandler PostAuthorizeRequest { add { } remove { } } + public event System.EventHandler PostLogRequest { add { } remove { } } + public event System.EventHandler PostMapRequestHandler { add { } remove { } } + public event System.EventHandler PostReleaseRequestState { add { } remove { } } + public event System.EventHandler PostRequestHandlerExecute { add { } remove { } } + public event System.EventHandler PostResolveRequestCache { add { } remove { } } + public event System.EventHandler PostUpdateRequestCache { add { } remove { } } + public event System.EventHandler PreRequestHandlerExecute { add { } remove { } } + public event System.EventHandler PreSendRequestHeaders { add { } remove { } } + public event System.EventHandler ReleaseRequestState { add { } remove { } } + 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 sealed partial class HttpApplicationState : System.Collections.Specialized.NameObjectCollectionBase + { + internal HttpApplicationState() { } + public string[] AllKeys { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public override int Count { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public object this[int index] { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public object this[string name] { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public void Add(string name, object value) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public void Clear() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public object Get(int index) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public object Get(string name) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public string GetKey(int index) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public void Lock() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public void Remove(string name) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public void RemoveAll() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public void RemoveAt(int index) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public void Set(string name, object value) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public void UnLock() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + } public partial class HttpBrowserCapabilities : System.Web.Configuration.HttpCapabilitiesBase { internal HttpBrowserCapabilities() { } @@ -49,9 +105,13 @@ public partial class HttpBrowserCapabilitiesWrapper : System.Web.HttpBrowserCapa public partial class HttpContext : System.IServiceProvider { internal HttpContext() { } + public System.Web.HttpApplicationState Application { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.HttpApplication ApplicationInstance { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public System.Web.Caching.Cache Cache { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public static System.Web.HttpContext Current { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.RequestNotification CurrentNotification { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public bool IsDebuggingEnabled { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public bool IsPostNotification { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public System.Collections.IDictionary Items { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public System.Web.HttpRequest Request { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public System.Web.HttpResponse Response { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } @@ -67,8 +127,12 @@ internal HttpContext() { } public partial class HttpContextBase : System.IServiceProvider { protected HttpContextBase() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public virtual System.Web.HttpApplicationState Application { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public virtual System.Web.HttpApplication ApplicationInstance { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual System.Web.Caching.Cache Cache { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public virtual System.Web.RequestNotification CurrentNotification { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual bool IsDebuggingEnabled { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public virtual bool IsPostNotification { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual System.Collections.IDictionary Items { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual System.Web.HttpRequestBase Request { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public virtual System.Web.HttpResponseBase Response { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } @@ -81,11 +145,16 @@ public partial class HttpContextBase : System.IServiceProvider public partial class HttpContextWrapper : System.Web.HttpContextBase { public HttpContextWrapper(System.Web.HttpContext httpContext) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public override System.Web.HttpApplicationState Application { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public override System.Web.HttpApplication ApplicationInstance { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.Web.Caching.Cache Cache { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public override System.Web.RequestNotification CurrentNotification { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override bool IsDebuggingEnabled { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public override bool IsPostNotification { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.Collections.IDictionary Items { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.Web.HttpRequestBase Request { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.Web.HttpResponseBase Response { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public override System.Web.HttpServerUtilityBase Server { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.Web.HttpSessionStateBase Session { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.DateTime Timestamp { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } public override System.Security.Principal.IPrincipal User { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } @@ -171,6 +240,17 @@ public partial class HttpFileCollectionWrapper : System.Web.HttpFileCollectionBa public override System.Collections.IEnumerator GetEnumerator() { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} public override System.Collections.Generic.IList GetMultiple(string name) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public sealed partial class HttpModuleCollection : System.Collections.Specialized.NameObjectCollectionBase + { + internal HttpModuleCollection() { } + public string[] AllKeys { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.IHttpModule this[int index] { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public System.Web.IHttpModule this[string name] { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public void CopyTo(System.Array dest, int index) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public System.Web.IHttpModule Get(int index) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public System.Web.IHttpModule Get(string name) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + public string GetKey(int index) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} + } public sealed partial class HttpPostedFile { internal HttpPostedFile() { } @@ -495,6 +575,11 @@ public sealed partial class HttpUnhandledException : System.Web.HttpException public HttpUnhandledException(string message) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} public HttpUnhandledException(string message, System.Exception innerException) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} } + public partial interface IHttpModule + { + void Dispose(); + void Init(System.Web.HttpApplication application); + } public partial interface ISubscriptionToken { bool IsActive { get; } @@ -507,6 +592,23 @@ public enum ReadEntityBodyMode Classic = 1, None = 0, } + [System.FlagsAttribute] + public enum RequestNotification + { + AcquireRequestState = 32, + AuthenticateRequest = 2, + AuthorizeRequest = 4, + BeginRequest = 1, + EndRequest = 2048, + ExecuteRequestHandler = 128, + LogRequest = 1024, + MapRequestHandler = 16, + PreExecuteRequestHandler = 64, + ReleaseRequestState = 256, + ResolveRequestCache = 8, + SendResponse = 536870912, + UpdateRequestCache = 512, + } public static partial class VirtualPathUtility { public static string AppendTrailingSlash(string virtualPath) { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs index c6112ae152..9e3ea8603e 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/Generated/TypeForwards.Framework.cs @@ -18,6 +18,8 @@ #pragma warning disable CA1063 // Implement IDisposable Correctly #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize +[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpApplication))] +[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpApplicationState))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpBrowserCapabilities))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpBrowserCapabilitiesBase))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpBrowserCapabilitiesWrapper))] @@ -30,6 +32,7 @@ [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpFileCollection))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpFileCollectionBase))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpFileCollectionWrapper))] +[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpModuleCollection))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpPostedFile))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpPostedFileBase))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpPostedFileWrapper))] @@ -46,8 +49,10 @@ [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpSessionStateBase))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpSessionStateWrapper))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.HttpUnhandledException))] +[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.IHttpModule))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.ISubscriptionToken))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.ReadEntityBodyMode))] +[assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.RequestNotification))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.VirtualPathUtility))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.Caching.Cache))] [assembly:System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Web.Caching.CacheDependency))] diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs new file mode 100644 index 0000000000..18e805dbe8 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplication.cs @@ -0,0 +1,318 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Security.Principal; +using System.Web.SessionState; +using Microsoft.AspNetCore.SystemWebAdapters; + +namespace System.Web; + +[Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = Constants.ApiFromAspNet)] +public class HttpApplication : IDisposable +{ + private const string HttpApplicationMustBeInitialized = "HttpApplication must be initialized before use."; + + private HttpApplicationState _state = null!; + private HttpContext? _context; + + private Dictionary? _events; + private HttpModuleCollection? _modules; + + public HttpApplication() + { + } + + internal void Initialize(HttpModuleCollection modules, HttpApplicationState state, Action eventInitializer) + { + _modules = modules; + _state = state; + + eventInitializer(this); + + foreach (var module in modules.Modules) + { + module.Init(this); + } + } + + public HttpModuleCollection Modules + => _modules ?? throw new InvalidOperationException(HttpApplicationMustBeInitialized); + + public HttpApplicationState Application + => _state ?? throw new InvalidOperationException(HttpApplicationMustBeInitialized); + + public HttpContext Context + { + get => _context ?? throw new InvalidOperationException("HttpContext can only be accessed during valid request."); + internal set => _context = value; + } + + public HttpRequest Request => Context.Request; + + public HttpResponse Response => Context.Response; + + public HttpServerUtility Server => Context.Server; + + [Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1065:Do not raise exceptions in unexpected locations", Justification = Constants.ApiFromAspNet)] + public HttpSessionState Session => Context.Session ?? throw new HttpException("Session is not available"); + + public IPrincipal User => Context.User; + + public void CompleteRequest() => Context.Response.End(); + + public event EventHandler? BeginRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? AuthenticateRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostAuthenticateRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? AuthorizeRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostAuthorizeRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? ResolveRequestCache + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostResolveRequestCache + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? MapRequestHandler + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostMapRequestHandler + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? AcquireRequestState + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostAcquireRequestState + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PreRequestHandlerExecute + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostRequestHandlerExecute + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? ReleaseRequestState + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostReleaseRequestState + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? UpdateRequestCache + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostUpdateRequestCache + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? LogRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PostLogRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? EndRequest + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? Error + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? Disposed + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + [Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = Constants.ApiFromAspNet)] + public void Dispose() + { + InvokeEvent(ApplicationEvent.Disposed); + + foreach (var module in Modules.Modules) + { + module.Dispose(); + } + } + + internal event EventHandler? SessionStart + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + internal event EventHandler? SessionEnd + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + internal event EventHandler? ApplicationStart + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + internal event EventHandler? ApplicationInit + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + public event EventHandler? PreSendRequestHeaders + { + add => AddEvent(value); + remove => RemoveEvent(value); + } + + private void AddEvent(EventHandler? handler, [CallerMemberName] string? name = null) + { + if (handler is null) + { + return; + } + + if (Enum.TryParse(name, out var eventName)) + { + AddEvent(eventName, handler); + } + else + { + throw new ArgumentOutOfRangeException(nameof(name)); + } + } + + private void RemoveEvent(EventHandler? handler, [CallerMemberName] string? name = null) + { + if (handler is null) + { + return; + } + + if (Enum.TryParse(name, out var eventName)) + { + RemoveEvent(eventName, handler); + } + else + { + throw new ArgumentOutOfRangeException(nameof(name)); + } + } + + private void AddEvent(ApplicationEvent appEvent, EventHandler handler) + { + _events ??= new(); + + if (_events.TryGetValue(appEvent, out var existing)) + { + _events[appEvent] = existing + handler; + } + else + { + _events.Add(appEvent, handler); + } + } + + private void RemoveEvent(ApplicationEvent appEvent, EventHandler handler) + { + if (_events is null) + { + return; + } + + if (_events.TryGetValue(appEvent, out var existing)) + { + if (existing - handler is { } updated) + { + _events[appEvent] = updated; + } + else + { + _events.Remove(appEvent); + } + } + } + + internal void InvokeEvent(ApplicationEvent appEvent) + { + if (_events is null) + { + return; + } + + if (_events.TryGetValue(appEvent, out var @event)) + { + @event(this, EventArgs.Empty); + } + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplicationState.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplicationState.cs new file mode 100644 index 0000000000..3cadac3bd8 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpApplicationState.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Threading; + +namespace System.Web; + +[SuppressMessage("Design", "CA1010:Generic interface should also be implemented", Justification = Constants.ApiFromAspNet)] +[SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "This is internally disposed")] +public sealed class HttpApplicationState : NameObjectCollectionBase +{ + private readonly ReaderWriterLockSlim _lock = new(); + + internal HttpApplicationState() + { + } + + public override int Count + { + get + { + _lock.EnterReadLock(); + try + { + return base.Count; + } + finally + { + _lock.ExitReadLock(); + } + } + } + + /// + /// Adds a new state object to the application state collection. + /// + public void Add(string name, object value) + { + _lock.EnterWriteLock(); + try + { + BaseAdd(name, value); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Updates an HttpApplicationState value within the collection. + /// + public void Set(string name, object value) + { + _lock.EnterWriteLock(); + try + { + BaseSet(name, value); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Removes an object from the application state collection by name. + /// + public void Remove(string name) + { + _lock.EnterWriteLock(); + try + { + BaseRemove(name); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Removes an object from the application state collection by name. + /// + public void RemoveAt(int index) + { + _lock.EnterWriteLock(); + try + { + BaseRemoveAt(index); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Removes all objects from the application state collection. + /// + public void Clear() + { + _lock.EnterWriteLock(); + try + { + BaseClear(); + } + finally + { + _lock.ExitWriteLock(); + } + } + + /// + /// Removes all objects from the application state collection. + /// + public void RemoveAll() => Clear(); + + /// + /// Gets an application state object by name. + /// + public object? Get(string name) + { + _lock.EnterReadLock(); + try + { + return BaseGet(name); + } + finally + { + _lock.ExitReadLock(); + } + } + + + /// + /// Gets or sets a single application state object. + /// + [DisallowNull] + public object? this[string name] + { + get { return Get(name); } + set { Set(name, value); } + } + + /// + /// Gets a single application state object by index. + /// + public object? Get(int index) + { + _lock.EnterReadLock(); + try + { + return BaseGet(index); + } + finally + { + _lock.ExitReadLock(); + } + } + + /// + /// Gets an application state object name by index. + /// + public string? GetKey(int index) + { + _lock.EnterReadLock(); + try + { + return BaseGetKey(index); + } + finally + { + _lock.ExitReadLock(); + } + } + + + /// + /// Gets an application state object by index. + /// + public object? this[int index] => Get(index); + + /// + /// Gets all application state object names in collection. + /// + [SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = Constants.ApiFromAspNet)] + public string?[]? AllKeys + { + get + { + _lock.EnterReadLock(); + try + { + return BaseGetAllKeys(); + } + finally + { + _lock.ExitReadLock(); + } + } + } + + /// + /// Locks access to all application state variables. Facilitates access synchronization. + /// + public void Lock() => _lock.EnterWriteLock(); + + /// + /// Unocks access to all application state variables. Facilitates access synchronization. + /// + public void UnLock() => _lock.ExitWriteLock(); + + /// + /// HttpApplication does not implement IDisposable since that wouldn't work for a .NET Standard build + /// + internal void Dispose() + { + _lock.Dispose(); + } +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs index 40885e8f03..860df49faa 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContext.cs @@ -41,6 +41,14 @@ internal HttpContext(HttpContextCore context) public HttpServerUtility Server => _server ??= new(_context); + public RequestNotification CurrentNotification => _context.Features.GetRequired().CurrentNotification; + + public bool IsPostNotification => _context.Features.GetRequired().IsPostNotification; + + public HttpApplication ApplicationInstance => _context.Features.GetRequired().Application; + + public HttpApplicationState Application => ApplicationInstance.Application; + public Cache Cache => _context.RequestServices.GetRequiredService(); /// diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextBase.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextBase.cs index 645d2ad2fd..066735a0d0 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextBase.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextBase.cs @@ -19,6 +19,14 @@ protected HttpContextBase() public virtual HttpResponseBase Response => throw new NotImplementedException(); + public virtual HttpApplication ApplicationInstance => throw new NotImplementedException(); + + public virtual HttpApplicationState Application => throw new NotImplementedException(); + + public virtual bool IsPostNotification => throw new NotImplementedException(); + + public virtual RequestNotification CurrentNotification => throw new NotImplementedException(); + public virtual IDictionary Items => throw new NotImplementedException(); public virtual DateTime Timestamp => throw new NotImplementedException(); diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextWrapper.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextWrapper.cs index a4fe5acadd..fe4aac6552 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextWrapper.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpContextWrapper.cs @@ -32,7 +32,17 @@ public HttpContextWrapper(HttpContext httpContext) public override HttpRequestBase Request => _request ??= new HttpRequestWrapper(_context.Request); public override HttpResponseBase Response => _response ??= new HttpResponseWrapper(_context.Response); - + + public override HttpApplication ApplicationInstance => _context.ApplicationInstance; + + public override HttpApplicationState Application => _context.Application; + + public override RequestNotification CurrentNotification => _context.CurrentNotification; + + public override bool IsPostNotification => _context.IsPostNotification; + + public override HttpServerUtilityBase Server => new HttpServerUtilityWrapper(_context.Server); + public override HttpSessionStateBase? Session { get diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpModuleCollection.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpModuleCollection.cs new file mode 100644 index 0000000000..42959ea115 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpModuleCollection.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Specialized; + +namespace System.Web; + +[Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1010:Generic interface should also be implemented", Justification = Constants.ApiFromAspNet)] +public sealed class HttpModuleCollection : NameObjectCollectionBase +{ + internal static HttpModuleCollection Empty { get; } = new(); + + private IHttpModule?[]? _all; + private string?[]? _allKeys; + + internal HttpModuleCollection() + : base(StringComparer.InvariantCultureIgnoreCase) + { + IsReadOnly = true; + } + + internal HttpModuleCollection(IEnumerable<(string Key, IHttpModule Module)> modules) + : base(StringComparer.InvariantCultureIgnoreCase) + { + foreach (var (name, module) in modules) + { + BaseAdd(name, module); + } + + IsReadOnly = true; + } + + public void CopyTo(Array dest, int index) + { + if (_all is null) + { + var n = Count; + _all = new IHttpModule?[n]; + + for (var i = 0; i < n; i++) + { + _all[i] = Get(i); + } + } + + _all.CopyTo(dest, index); + } + + internal IEnumerable Modules + { + get + { + for (var i = 0; i < Count; i++) + { + if (Get(i) is IHttpModule module) + { + yield return module; + } + } + } + } + + public IHttpModule? Get(string name) => (IHttpModule?)BaseGet(name); + + public IHttpModule? this[string name] => Get(name); + + public IHttpModule? Get(int index) => (IHttpModule?)BaseGet(index); + + public string? GetKey(int index) => BaseGetKey(index); + + public IHttpModule? this[int index] => Get(index); + + [Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = Constants.ApiFromAspNet)] + public string?[] AllKeys => _allKeys ??= BaseGetAllKeys(); +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs index 3da76dd624..0b742d124a 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/HttpRequest.cs @@ -9,7 +9,6 @@ using System.Net; using System.Security.Principal; using System.Text; -using System.Web.Configuration; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/IHttpModule.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/IHttpModule.cs new file mode 100644 index 0000000000..6f200e8cec --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/IHttpModule.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Web; + +public interface IHttpModule +{ + void Init(HttpApplication application); + + void Dispose(); +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/RequestNotification.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/RequestNotification.cs new file mode 100644 index 0000000000..17159f2849 --- /dev/null +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/RequestNotification.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Web; + +[Flags] +public enum RequestNotification +{ + BeginRequest = 0x00000001, // request is beginning + AuthenticateRequest = 0x00000002, // request is being authenticated + AuthorizeRequest = 0x00000004, // request is being authorized + ResolveRequestCache = 0x00000008, // satisfy request from cache + MapRequestHandler = 0x00000010, // map handler for request + AcquireRequestState = 0x00000020, // acquire request state + PreExecuteRequestHandler = 0x00000040, + ExecuteRequestHandler = 0x00000080, // execute handler + ReleaseRequestState = 0x00000100, // release request state + UpdateRequestCache = 0x00000200, // update cache + LogRequest = 0x00000400, // log request + EndRequest = 0x00000800, // end request + SendResponse = 0x20000000 // send response +} diff --git a/src/Microsoft.AspNetCore.SystemWebAdapters/SessionState/HttpSessionState.cs b/src/Microsoft.AspNetCore.SystemWebAdapters/SessionState/HttpSessionState.cs index 18219c0d95..73a2ff7212 100644 --- a/src/Microsoft.AspNetCore.SystemWebAdapters/SessionState/HttpSessionState.cs +++ b/src/Microsoft.AspNetCore.SystemWebAdapters/SessionState/HttpSessionState.cs @@ -10,59 +10,59 @@ namespace System.Web.SessionState; [Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = Constants.ApiFromAspNet)] public class HttpSessionState : ICollection { - private readonly ISessionState _container; - public HttpSessionState(ISessionState container) { - _container = container; + State = container; } - public string SessionID => _container.SessionID; + internal ISessionState State { get; } + + public string SessionID => State.SessionID; - public int Count => _container.Count; + public int Count => State.Count; - public bool IsReadOnly => _container.IsReadOnly; + public bool IsReadOnly => State.IsReadOnly; - public bool IsNewSession => _container.IsNewSession; + public bool IsNewSession => State.IsNewSession; public int Timeout { - get => _container.Timeout; - set => _container.Timeout = value; + get => State.Timeout; + set => State.Timeout = value; } - public bool IsSynchronized => _container.IsSynchronized; + public bool IsSynchronized => State.IsSynchronized; - public object SyncRoot => _container.SyncRoot; + public object SyncRoot => State.SyncRoot; [Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = Constants.ApiFromAspNet)] public SessionStateMode Mode => SessionStateMode.Custom; - public void Abandon() => _container.IsAbandoned = true; + public void Abandon() => State.IsAbandoned = true; public object? this[string name] { - get => _container[name]; - set => _container[name] = value; + get => State[name]; + set => State[name] = value; } - public void Add(string name, object value) => _container[name] = value; + public void Add(string name, object value) => State[name] = value; - public void Remove(string name) => _container.Remove(name); + public void Remove(string name) => State.Remove(name); - public void RemoveAll() => _container.Clear(); + public void RemoveAll() => State.Clear(); - public void Clear() => _container.Clear(); + public void Clear() => State.Clear(); public void CopyTo(Array array, int index) { ArgumentNullException.ThrowIfNull(array); - foreach (var key in _container.Keys) + foreach (var key in State.Keys) { - array.SetValue(_container[key], index++); + array.SetValue(State[key], index++); } } - public IEnumerator GetEnumerator() => _container.Keys.GetEnumerator(); + public IEnumerator GetEnumerator() => State.Keys.GetEnumerator(); } 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 01a2aa88d9..8186c472bc 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 @@ -11,6 +11,13 @@ + + + + + + diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs new file mode 100644 index 0000000000..6f5cac863a --- /dev/null +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/Modules/ModuleTests.cs @@ -0,0 +1,184 @@ +// 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.Linq; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ModulesLibrary; +using Xunit; + +namespace Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests; + +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), + }; + + public static IEnumerable GetAllEvents() + => Initial.Concat(Always).Select(o => new[] { o }); + + [MemberData(nameof(GetAllEvents))] + [Theory] + public async Task EndModuleEarly(string notification) + { + var expected = GetNotificationsUpTo(notification); + var result = await RunAsync(ModuleTestModule.End, notification); + + Assert.Equal(expected, result); + } + + [MemberData(nameof(GetAllEvents))] + [Theory] + public async Task CompleteModuleEarly(string notification) + { + var expected = GetNotificationsUpTo(notification); + var result = await RunAsync(ModuleTestModule.Complete, notification); + + Assert.Equal(expected, result); + } + + [MemberData(nameof(GetAllEvents))] + [Theory] + public async Task ModulesThrow(string notification) + { + var expected = GetExpected(notification).ToList(); + var result = await RunAsync(ModuleTestModule.Throw, notification); + + Assert.Equal(expected, result); + + static IEnumerable GetExpected(string notification) + { + foreach (var item in GetNotificationsUpTo(notification)) + { + yield return item; + + if (string.Equals(item, notification, StringComparison.Ordinal)) + { + yield return nameof(HttpApplication.Error); + } + } + } + } + + private static IEnumerable GetNotificationsUpTo(string notification) + { + foreach (var n in Initial) + { + yield return n; + + if (string.Equals(n, notification, StringComparison.Ordinal)) + { + break; + } + } + + foreach (var n in Always) + { + yield return n; + } + } + + private static async Task> RunAsync(string action, string eventName) + { + 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.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(); + + app.Run(ctx => Task.CompletedTask); + }); + }) + .StartAsync(); + + var url = $"/?action={action}¬ification={eventName}"; + + using var _ = await host.GetTestClient().GetAsync(new Uri(url, UriKind.Relative)); + + return notifier; + } + + private sealed class NotificationCollection : List + { + } + + private sealed class ModuleTestModule : EventsModule + { + protected override void InvokeEvent(HttpContext context, string name) + { + Add(context, name); + base.InvokeEvent(context, name); + } + + private static void Add(HttpContextCore context, string name) + { + context.Features.GetRequired().Add(name); + } + } +} diff --git a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/ResponseStreamTests.cs b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/ResponseStreamTests.cs index ed26fa8a71..9af20db8e4 100644 --- a/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/ResponseStreamTests.cs +++ b/test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/ResponseStreamTests.cs @@ -40,6 +40,42 @@ public async Task BufferedOutputIsFlushed() Assert.Equal(ContentValue, result); } + [Fact] + public async Task BufferedOutputIsFlushedOnceWithStart() + { + var result = await RunAsync(async context => + { + context.Response.Write(ContentValue); + await ((HttpResponseCore)context.Response).StartAsync(); + }, builder => builder.BufferResponseStream()); + + Assert.Equal(ContentValue, result); + } + + [Fact] + public async Task BufferedOutputIsFlushedOnceWithComplete() + { + var result = await RunAsync(async context => + { + context.Response.Write(ContentValue); + await ((HttpResponseCore)context.Response).CompleteAsync(); + }, builder => builder.BufferResponseStream()); + + Assert.Equal(ContentValue, result); + } + + [Fact] + public async Task EndRequest() + { + var result = await RunAsync(context => + { + context.Response.Write("test"); + context.Response.End(); + }, builder => builder.BufferResponseStream()); + + Assert.Equal("test", result); + } + [Fact] public async Task SuppressContent() { @@ -127,9 +163,16 @@ public async Task MultipleClearContent() Assert.Equal("part4", result); } - private static async Task RunAsync(Action action, Action? route = null) + private static Task RunAsync(Action action, Action? builder = null) + => RunAsync(ctx => + { + action(ctx); + return Task.CompletedTask; + }, builder); + + private static async Task RunAsync(Func action, Action? builder = null) { - route ??= _ => { }; + builder ??= _ => { }; using var host = await new HostBuilder() .ConfigureWebHost(webBuilder => @@ -150,7 +193,7 @@ private static async Task RunAsync(Action action, Action { - route(endpoints.Map("/", (HttpContextCore context) => action(context))); + builder(endpoints.Map("/", (HttpContextCore context) => action(context))); }); }); })