Skip to main content

Introducing System.Web Adapters v1.2 with new APIs and scenarios

Today, we’re releasing an update to the System.Web Adapters that simplify upgrading from ASP.NET to ASP.NET Core. This release brings a number of fixes as well as new scenarios that we’ll explore in this post.

IHttpModule support and emulation in ASP.NET Core

One of the scenarios this release enables is a way to run custom HttpApplication and managed IHttpModule implementations in the ASP.NET Core pipeline. Ideally, these would be refactored to middleware in ASP.NET Core, but we’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.

You add HttpApplication and IHttpModule implementations using the System.Web adapter builder:

using System.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSystemWebAdapters()
    // Without the generic argument, a default HttpApplication will be used
    .AddHttpApplication<MyApp>(options =>
    {
        // Size of pool for HttpApplication instances. Should be what the expected concurrent requests will be
        options.PoolSize = 10;

        // Register a module by name - without the name, it will default to the type name
        options.RegisterModule<MyModule>("MyModule");
    });

var app = builder.Build();

app.UseSystemWebAdapters();

app.Run();

class MyApp : HttpApplication
{
    protected void Application_Start()
    {
        ...
    }
}

internal sealed class MyModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        application.AuthorizeRequest += (s, e)
        {
            ...
        }

        application.BeginRequest += (s, e) =>
        {
            ...
        }

        application.EndRequest += (s, e) =>
        {
            ...
        }
    }

    public void Dispose()
    {
    }
}

Some things to keep in mind while using this feature:

  • 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.
  • In order to have the authorization and authentication related events run when expected, additional middleware should be manually inserted by calling UseAuthenticationEvents() and UseAuthorizationEvents(). If this is not done, the middleware will be automatically inserted when UseSystemWebAdapters() is called, which may cause these events to fire at unexpected times:
    var app = builder.Build();

    app.UseAuthentication();
+   app.UseAuthenticationEvents();
    app.UseAuthorization();
+   app.UseAuthorizationEvents();
    app.UseSystemWebAdapters();

    app.Run();
  • 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’re not aware at this moment of major differences, but please file issues at dotnet/systemweb-adapters if you find any.
  • If HttpApplication.GetVaryByCustomString(...) was customized and expected, it may be hooked up to the output caching availabe in .NET 7 and later via some provided extension methods. See the module sample for examples on how to set this up.
  • HttpContext.Error and other exception related HttpContext APIs are now hooked up to be used as expected to control any errors that occurs while invoking the events.

Custom session key serializers

When using the System.Web adapters you can customize the serialization of session values using the ISessionKeySerializer interface. With this release you can now register multiple implementations of ISessionKeySerializer, 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 Nullable<> values, can now be serialized.

The example below demonstrates how to customize the serialization of session values using multiple ISessionKeySerializer implementations:

using Microsoft.AspNetCore.SystemWebAdapters;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

using HttpContext = System.Web.HttpContext;
using HttpContextCore = Microsoft.AspNetCore.Http.HttpContext;

internal static class SessionExampleExtensions
{
    private const string SessionKey = "array";

    public static ISystemWebAdapterBuilder AddCustomSerialization(this ISystemWebAdapterBuilder builder)
    {
        builder.Services.AddSingleton<ISessionKeySerializer>(new ByteArraySerializer(SessionKey));
        return builder.AddJsonSessionSerializer(options =>
        {
            options.RegisterKey<int>("callCount");
        });
    }

    public static void MapSessionExample(this RouteGroupBuilder builder)
    {
        builder.RequireSystemWebAdapterSession();

        builder.MapGet("/custom", (HttpContextCore ctx) =>
        {
            return GetValue(ctx);

            static object? GetValue(HttpContext context)
            {
                if (context.Session![SessionKey] is { } existing)
                {
                    return existing;
                }

                var temp = new byte[] { 1, 2, 3 };
                context.Session[SessionKey] = temp;
                return temp;
            }
        });

        builder.MapPost("/custom", async (HttpContextCore ctx) =>
        {
            using var ms = new MemoryStream();
            await ctx.Request.Body.CopyToAsync(ms);

            SetValue(ctx, ms.ToArray());

            static void SetValue(HttpContext context, byte[] data)
                => context.Session![SessionKey] = data;
        });

        builder.MapGet("/count", (HttpContextCore ctx) =>
        {
            var context = (HttpContext)ctx;

            if (context.Session!["callCount"] is not int count)
            {
                count = 0;
            }

            context.Session!["callCount"] = ++count;

            return $"This endpoint has been hit {count} time(s) this session";
        });
    }

    /// <summary>
    /// This is an example of a custom <see cref="ISessionKeySerializer"/> that takes a key name and expects the value to be a byte array.
    /// </summary>
    private sealed class ByteArraySerializer : ISessionKeySerializer
    {
        private readonly string _key;

        public ByteArraySerializer(string key)
        {
            _key = key;
        }

        public bool TryDeserialize(string key, byte[] bytes, out object? obj)
        {
            if (string.Equals(_key, key, StringComparison.Ordinal))
            {
                obj = bytes;
                return true;
            }

            obj = null;
            return false;
        }

        public bool TrySerialize(string key, object? value, out byte[] bytes)
        {
            if (string.Equals(_key, key, StringComparison.Ordinal) && value is byte[] valueBytes)
            {
                bytes = valueBytes;
                return true;
            }

            bytes = Array.Empty<byte>();
            return false;
        }
    }
}

IHtmlString support

We’ve added System.Web.IHtmlString support to .NET 8 to enable scenarios where people may be relying on it for System.Web.HtmlUtility behavior. As part of this, the adapters now contain System.Web.HtmlString, as well as a .NET Standard 2.0 System.Web.IHtmlString to facillitate usage in migration scenarios. IHtmlString 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.

Additional APIs

A number of additional APIs have been added:

  • IHttpModule, HttpApplication, HttpApplicationState and other module related types
  • Additional overloads of HttpContext.RewritePath
  • Expansion of the HttpContextBase, HttpRequestBase, and HttpResponseBase types
  • HttpRequest.FilePath and HttpContext.PathInfo is now supported via the HttpContext.RewritePath

We want to thank Steven De Kock, Ruiyang Li, Clounea, and Cynthia MacLeod for their contributions to this release!

Incremental migration guidance

As part of this release, we’re also updating some guidance around incremental migration. Some of the key areas are:

Improved Blazor fallback routing

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’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 & 7 we now have guidance on how to refine the Blazor fallback route so that it works with incremental migration.

Incremental ASP.NET Web Forms migration

Upgrading from ASP.NET Web Forms to ASP.NET Core is challenging because ASP.NET Core doesn’t support the Web Forms programming model. You can incrementally upgrade .aspx pages to ASP.NET Core, but you’ll need to reimplement the UI rendering logic using a supported ASP.NET Core framework, like Razor Pages or Blazor.

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, Razor components may be used to incrementally migrate Web Forms controls to Blazor controls and place them on .aspx pages instead.

For an example of this, see the sample in the dotnet/systemweb-adapters repo.

A/B Testing of Migrated Endpoints

As we worked with customers to try out the migration recommendations, a common thread emerged as to how to validate endpoints. We’ve added some docs 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’re not happy with the migrated implementation.

Summary

Release v1.2 of the System.Web adapters brings some new features and bug fixes, including support for simpler migration of IHttpModule implementations. Please engage with us at https://github.com/dotnet/systemweb-adapters – we welcome any issues you face and/or PRs to help move it forward!

The post Introducing System.Web Adapters v1.2 with new APIs and scenarios appeared first on .NET Blog.



Comments

Popular posts from this blog

Fake CVR Generator Denmark

What Is Danish CVR The Central Business Register (CVR) is the central register of the state with information on all Danish companies. Since 1999, the Central Business Register has been the authoritative register for current and historical basic data on all registered companies in Denmark. Data comes from the companies' own registrations on Virk Report. There is also information on associations and public authorities in the CVR. As of 2018, CVR also contains information on Greenlandic companies, associations and authorities. In CVR at Virk you can do single lookups, filtered searches, create extracts and subscriptions, and retrieve a wide range of company documents and transcripts. Generate Danish CVR For Test (Fake) Click the button below to generate the valid CVR number for Denmark. You can click multiple times to generate several numbers. These numbers can be used to Test your sofware application that uses CVR, or Testing CVR APIs that Danish Govt provide. Generate

How To Iterate Dictionary Object

Dictionary is a object that can store values in Key-Value pair. its just like a list, the only difference is: List can be iterate using index(0-n) but not the Dictionary . Generally when we try to iterate the dictionary we get below error: " Collection was modified; enumeration operation may not execute. " So How to parse a dictionary and modify its values?? To iterate dictionary we must loop through it's keys or key - value pair. Using keys

How To Append Data to HTML5 localStorage or sessionStorage?

The localStorage property allows you to access a local Storage object. localStorage is similar to sessionStorage. The only difference is that, while data stored in localStorage has no expiration time untill unless user deletes his cache, data stored in sessionStorage gets cleared when the originating window or tab get closed. These are new HTML5 objects and provide these methods to deal with it: The following snippet accesses the current domain's local Storage object and adds a data item to it using Storage.setItem() . localStorage.setItem('myFav', 'Taylor Swift'); or you can use the keyname directly as : localStorage.myFav = 'Taylor Swift'; To grab the value set in localStorage or sessionStorage, we can use localStorage.getItem("myFav"); or localStorage.myFav There's no append function for localStorage or sessionStorage objects. It's not hard to write one though.The simplest solution goes here: But we can kee