{"id":46427,"date":"2023-07-10T10:05:00","date_gmt":"2023-07-10T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=46427"},"modified":"2024-12-13T14:12:18","modified_gmt":"2024-12-13T22:12:18","slug":"systemweb-adapters-1_2","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/systemweb-adapters-1_2\/","title":{"rendered":"Introducing System.Web Adapters v1.2 with new APIs and scenarios"},"content":{"rendered":"<p>Today, we&#8217;re releasing an update to the <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.AspNetCore.SystemWebAdapters\">System.Web Adapters<\/a> that simplify upgrading from ASP.NET to ASP.NET Core. This release brings a number of fixes as well as new scenarios that we&#8217;ll explore in this post.<\/p>\n<h2><code>IHttpModule<\/code> support and emulation in ASP.NET Core<\/h2>\n<p>One of the scenarios this release enables is a way to run custom <code>HttpApplication<\/code> and managed <code>IHttpModule<\/code> implementations in the ASP.NET Core pipeline. Ideally, these would be refactored to middleware in ASP.NET Core, but we&#8217;ve seen instances where this can be a blocker to migration. This new support allows more shared code to be migrated to ASP.NET Core, although there may be some behavior differences that cannot be handled in the ASP.NET Core pipeline.<\/p>\n<p>You add <code>HttpApplication<\/code> and <code>IHttpModule<\/code> implementations using the System.Web adapter builder:<\/p>\n<pre><code class=\"language-csharp\">using System.Web;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddSystemWebAdapters()\r\n    \/\/ Without the generic argument, a default HttpApplication will be used\r\n    .AddHttpApplication&lt;MyApp&gt;(options =&gt;\r\n    {\r\n        \/\/ Size of pool for HttpApplication instances. Should be what the expected concurrent requests will be\r\n        options.PoolSize = 10;\r\n\r\n        \/\/ Register a module by name - without the name, it will default to the type name\r\n        options.RegisterModule&lt;MyModule&gt;(\"MyModule\");\r\n    });\r\n\r\nvar app = builder.Build();\r\n\r\napp.UseSystemWebAdapters();\r\n\r\napp.Run();\r\n\r\nclass MyApp : HttpApplication\r\n{\r\n    protected void Application_Start()\r\n    {\r\n        ...\r\n    }\r\n}\r\n\r\ninternal sealed class MyModule : IHttpModule\r\n{\r\n    public void Init(HttpApplication app)\r\n    {\r\n        application.AuthorizeRequest += (s, e)\r\n        {\r\n            ...\r\n        }\r\n\r\n        application.BeginRequest += (s, e) =&gt;\r\n        {\r\n            ...\r\n        }\r\n\r\n        application.EndRequest += (s, e) =&gt;\r\n        {\r\n            ...\r\n        }\r\n    }\r\n\r\n    public void Dispose()\r\n    {\r\n    }\r\n}<\/code><\/pre>\n<p>Some things to keep in mind while using this feature:<\/p>\n<ul>\n<li>Simple modules (especially those with only a single event), should be migrated to middleware instead using the System.Web adapters to share code as needed.<\/li>\n<li>In order to have the authorization and authentication related events run when expected, additional middleware should be manually inserted by calling <code>UseAuthenticationEvents()<\/code> and <code>UseAuthorizationEvents()<\/code>. If this is not done, the middleware will be automatically inserted when <code>UseSystemWebAdapters()<\/code> is called, which may cause these events to fire at unexpected times:<\/li>\n<\/ul>\n<pre><code class=\"language-diff\">    var app = builder.Build();\r\n\r\n    app.UseAuthentication();\r\n+   app.UseAuthenticationEvents();\r\n    app.UseAuthorization();\r\n+   app.UseAuthorizationEvents();\r\n    app.UseSystemWebAdapters();\r\n\r\n    app.Run();<\/code><\/pre>\n<ul>\n<li>The events are fired in the order they were in ASP.NET, but some of the state of the request may not be quite the same due to underlying differences in the frameworks. We&#8217;re not aware at this moment of major differences, but please file issues at <a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\">dotnet\/systemweb-adapters<\/a> if you find any.<\/li>\n<li>If <code>HttpApplication.GetVaryByCustomString(...)<\/code> was customized and expected, it may be hooked up to the <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/performance\/caching\/output\">output caching<\/a> availabe in .NET 7 and later via some provided extension methods. See the <a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\/blob\/main\/samples\/Modules\/ModulesCore\/Program.cs\">module sample<\/a> for examples on how to set this up.<\/li>\n<li><code>HttpContext.Error<\/code> and other exception related <code>HttpContext<\/code> APIs are now hooked up to be used as expected to control any errors that occurs while invoking the events.<\/li>\n<\/ul>\n<h2>Custom session key serializers<\/h2>\n<p>When using the System.Web adapters you can customize the serialization of session values using the <code>ISessionKeySerializer<\/code> interface. With this release you can now register multiple implementations of <code>ISessionKeySerializer<\/code>, and the adapters will iterate through all of them to identify how to serialize a given key. Previous versions would only use the latest registered serializer, which made it difficult to compose different, independent serializers. Now we attempt to use each registered serializer until one succeeds. Null values, including <code>Nullable&lt;&gt;<\/code> values, can now be serialized.<\/p>\n<p>The example below demonstrates how to customize the serialization of session values using multiple <code>ISessionKeySerializer<\/code> implementations:<\/p>\n<pre><code class=\"language-csharp\">using Microsoft.AspNetCore.SystemWebAdapters;\r\nusing Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;\r\n\r\nusing HttpContext = System.Web.HttpContext;\r\nusing HttpContextCore = Microsoft.AspNetCore.Http.HttpContext;\r\n\r\ninternal static class SessionExampleExtensions\r\n{\r\n    private const string SessionKey = \"array\";\r\n\r\n    public static ISystemWebAdapterBuilder AddCustomSerialization(this ISystemWebAdapterBuilder builder)\r\n    {\r\n        builder.Services.AddSingleton&lt;ISessionKeySerializer&gt;(new ByteArraySerializer(SessionKey));\r\n        return builder.AddJsonSessionSerializer(options =&gt;\r\n        {\r\n            options.RegisterKey&lt;int&gt;(\"callCount\");\r\n        });\r\n    }\r\n\r\n    public static void MapSessionExample(this RouteGroupBuilder builder)\r\n    {\r\n        builder.RequireSystemWebAdapterSession();\r\n\r\n        builder.MapGet(\"\/custom\", (HttpContextCore ctx) =&gt;\r\n        {\r\n            return GetValue(ctx);\r\n\r\n            static object? GetValue(HttpContext context)\r\n            {\r\n                if (context.Session![SessionKey] is { } existing)\r\n                {\r\n                    return existing;\r\n                }\r\n\r\n                var temp = new byte[] { 1, 2, 3 };\r\n                context.Session[SessionKey] = temp;\r\n                return temp;\r\n            }\r\n        });\r\n\r\n        builder.MapPost(\"\/custom\", async (HttpContextCore ctx) =&gt;\r\n        {\r\n            using var ms = new MemoryStream();\r\n            await ctx.Request.Body.CopyToAsync(ms);\r\n\r\n            SetValue(ctx, ms.ToArray());\r\n\r\n            static void SetValue(HttpContext context, byte[] data)\r\n                =&gt; context.Session![SessionKey] = data;\r\n        });\r\n\r\n        builder.MapGet(\"\/count\", (HttpContextCore ctx) =&gt;\r\n        {\r\n            var context = (HttpContext)ctx;\r\n\r\n            if (context.Session![\"callCount\"] is not int count)\r\n            {\r\n                count = 0;\r\n            }\r\n\r\n            context.Session![\"callCount\"] = ++count;\r\n\r\n            return $\"This endpoint has been hit {count} time(s) this session\";\r\n        });\r\n    }\r\n\r\n    \/\/\/ &lt;summary&gt;\r\n    \/\/\/ This is an example of a custom &lt;see cref=\"ISessionKeySerializer\"\/&gt; that takes a key name and expects the value to be a byte array.\r\n    \/\/\/ &lt;\/summary&gt;\r\n    private sealed class ByteArraySerializer : ISessionKeySerializer\r\n    {\r\n        private readonly string _key;\r\n\r\n        public ByteArraySerializer(string key)\r\n        {\r\n            _key = key;\r\n        }\r\n\r\n        public bool TryDeserialize(string key, byte[] bytes, out object? obj)\r\n        {\r\n            if (string.Equals(_key, key, StringComparison.Ordinal))\r\n            {\r\n                obj = bytes;\r\n                return true;\r\n            }\r\n\r\n            obj = null;\r\n            return false;\r\n        }\r\n\r\n        public bool TrySerialize(string key, object? value, out byte[] bytes)\r\n        {\r\n            if (string.Equals(_key, key, StringComparison.Ordinal) &amp;&amp; value is byte[] valueBytes)\r\n            {\r\n                bytes = valueBytes;\r\n                return true;\r\n            }\r\n\r\n            bytes = Array.Empty&lt;byte&gt;();\r\n            return false;\r\n        }\r\n    }\r\n}<\/code><\/pre>\n<h2>IHtmlString support<\/h2>\n<p>We&#8217;ve added <code>System.Web.IHtmlString<\/code> support to .NET 8 to enable scenarios where people may be relying on it for <code>System.Web.HtmlUtility<\/code> behavior. As part of this, the adapters now contain <code>System.Web.HtmlString<\/code>, as well as a .NET Standard 2.0 <code>System.Web.IHtmlString<\/code> to facillitate usage in migration scenarios. <code>IHtmlString<\/code> currently forwards on framework to the in box version, and when .NET 8 is released will forward to that one as well allowing seamless use of the type in upgrade scenarios.<\/p>\n<h2>Additional APIs<\/h2>\n<p>A number of additional APIs have been added:<\/p>\n<ul>\n<li><code>IHttpModule<\/code>, <code>HttpApplication<\/code>, <code>HttpApplicationState<\/code> and other module related types<\/li>\n<li>Additional overloads of <code>HttpContext.RewritePath<\/code><\/li>\n<li>Expansion of the <code>HttpContextBase<\/code>, <code>HttpRequestBase<\/code>, and <code>HttpResponseBase<\/code> types<\/li>\n<li><code>HttpRequest.FilePath<\/code> and <code>HttpContext.PathInfo<\/code> is now supported via the <code>HttpContext.RewritePath<\/code><\/li>\n<\/ul>\n<p>We want to thank <a href=\"https:\/\/github.com\/sdekock\">Steven De Kock<\/a>, <a href=\"https:\/\/github.com\/Elderry\">Ruiyang Li<\/a>, <a href=\"https:\/\/github.com\/Clounea\">Clounea<\/a>, and <a href=\"https:\/\/github.com\/CZEMacLeod\">Cynthia MacLeod<\/a> for their contributions to this release!<\/p>\n<h2>Incremental migration guidance<\/h2>\n<p>As part of this release, we&#8217;re also updating some guidance around <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/migration\/inc\/overview\">incremental migration<\/a>. Some of the key areas are:<\/p>\n<h3>Improved Blazor fallback routing<\/h3>\n<p>Blazor apps typically use a fallback route that routes any requests to the root of the app so they can be handled by client-side routing. This makes it difficult to use Blazor for incremental migration because YARP doesn&#8217;t get a chance to proxy unhandled requests. In .NET 8 the routing support in Blazor is getting improved to handle this situation better, but for .NET 6 &amp; 7 we now have <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/migration\/inc\/blazor\">guidance<\/a> on how to refine the Blazor fallback route so that it works with incremental migration.<\/p>\n<h3>Incremental ASP.NET Web Forms migration<\/h3>\n<p>Upgrading from ASP.NET Web Forms to ASP.NET Core is challenging because ASP.NET Core doesn&#8217;t support the Web Forms programming model. You can incrementally upgrade .aspx pages to ASP.NET Core, but you&#8217;ll need to reimplement the UI rendering logic using a supported ASP.NET Core framework, like Razor Pages or Blazor.<\/p>\n<p>With .NET 7 you can now incrementally replace Web Forms controls on a page with Blazor components using the new custom elements support. If using the incremental migration approach with YARP, <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/blazor\/components\/js-spa-frameworks\">Razor components<\/a> may be used to incrementally migrate Web Forms controls to Blazor controls and place them on <code>.aspx<\/code> pages instead.<\/p>\n<p>For an example of this, see the <a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\/tree\/main\/samples\/WebFormsToBlazor\">sample<\/a> in the <code>dotnet\/systemweb-adapters<\/code> repo.<\/p>\n<h3>A\/B Testing of Migrated Endpoints<\/h3>\n<p>As we worked with customers to try out the migration recommendations, a common thread emerged as to how to validate endpoints. We&#8217;ve added some <a href=\"https:\/\/learn.microsoft.com\/aspnet\/core\/migration\/inc\/abtesting\">docs<\/a> on how to disable endpoints at runtime to fallback to the ASP.NET application. This can be used in cases where you want to A\/B test for a given population, or if you decide you&#8217;re not happy with the migrated implementation.<\/p>\n<h2>Summary<\/h2>\n<p>Release v1.2 of the System.Web adapters brings some new features and bug fixes, including support for simpler migration of <code>IHttpModule<\/code> implementations. Please engage with us at https:\/\/github.com\/dotnet\/systemweb-adapters &#8211; we welcome any issues you face and\/or PRs to help move it forward!<\/p>\n<h2>Additional links<\/h2>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\">GitHub project<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/systemweb-adapters\/tree\/main\/samples\">Incremental migration samples<\/a><\/li>\n<li><a href=\"https:\/\/docs.microsoft.com\/aspnet\/core\/migration\/proper-to-2x\">Migrate from ASP.NET to ASP.NET Core docs<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Introducing the release of System.Web adapters v1.2 which introduces new APIs, better Blazor support, A\/B testing of migrated endpoints, and more.<\/p>\n","protected":false},"author":92886,"featured_media":46432,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,195,197,7509],"tags":[7751,31,3267,7750],"class_list":["post-46427","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-dotnet-framework","category-aspnet","category-aspnetcore","tag-adapters","tag-asp-net","tag-migration","tag-system-web"],"acf":[],"blog_post_summary":"<p>Introducing the release of System.Web adapters v1.2 which introduces new APIs, better Blazor support, A\/B testing of migrated endpoints, and more.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/46427","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/92886"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=46427"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/46427\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/46432"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=46427"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=46427"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=46427"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}