将 ASP.NET 框架会话迁移到 ASP.NET Core

会话状态是许多 Web 应用程序的关键组件,跨 HTTP 请求存储特定于用户的数据。 从 ASP.NET Framework 迁移到 ASP.NET Core 时,会话状态会带来独特的挑战,因为两个框架处理会话的方式大相径庭。

为什么会话迁移很复杂

ASP.NET Framework 和 ASP.NET Core 具有完全不同的会话管理方法:

  • ASP.NET Framework 提供自动对象序列化和内置会话锁定
  • ASP.NET Core 需要手动序列化,并且不提供会话锁定保证

这些差异意味着您无法在不进行更改的情况下将会话代码从 Framework 移动到 Core。

迁移策略概述

在迁移过程中,有三种处理会话状态的主要方法:

  1. 完全重写 - 重写所有会话代码以使用 ASP.NET Core 的本机会话实现
  2. 使用单独的会话进行增量 - 逐条迁移组件,每个应用都保持自己的会话状态
  3. 增量式共享会话 - 在 Framework 和 Core 应用程序之间共享会话数据的同时迁移组件

对于大多数应用程序,迁移到 ASP.NET Core 会话 可提供最佳性能和可维护性。 但是,较大的应用程序或具有复杂会话要求的应用程序可能受益于使用 System.Web 适配器的增量方法。

选择迁移方法

有三个主要选项用于将会话状态从 ASP.NET Framework 迁移到 ASP.NET Core。 选择取决于迁移时间线、是否需要同时运行这两个应用程序,以及你愿意重写的代码量。

快速决策指南

回答以下问题以选择你的方法:

  1. 是否执行完全重写或增量迁移?

  2. ASP.NET Framework 和 ASP.NET Core 应用是否需要访问相同的会话数据?

了解差异

在深入了解实现详细信息之前,请务必了解 ASP.NET Framework 和 ASP.NET Core 如何处理会话状态的不同:

  • 对象序列化
    • ASP.NET Framework 自动序列化和反序列化对象(除非使用内存中存储)
    • ASP.NET Core 需要手动序列化/反序列化,并将数据存储为 byte[]
  • 会话锁定
    • ASP.NET 框架在会话中锁定会话使用情况,并串行处理后续请求
    • ASP.NET Core 不提供会话锁定保证

迁移方法比较

方法 代码更改 性能 会话共享 何时使用
内置 ASP.NET 核心 高 - 重写所有会话代码 最佳 没有 完全重写,性能关键型应用
包装的 ASP.NET Core 低 - 保留现有会话模式 没有 增量迁移,无需共享状态
远程应用 低 - 保留现有会话模式 一般 完整 同时运行这两个应用

System.Web 适配器通过两个关键接口桥接 ASP.NET Framework 和 ASP.NET Core 会话实现之间的差异,实现“包装”和“远程应用”方法:

  • Microsoft.AspNetCore.SystemWebAdapters.ISessionManager:接受一个HttpContext对象和会话元数据,返回一个ISessionState对象
  • Microsoft.AspNetCore.SystemWebAdapters.ISessionState:描述会话对象状态并支持 HttpSessionState 类型

内置 ASP.NET 核心会话状态

执行完整迁移时,请选择此方法,并可以重写与会话相关的代码以使用 ASP.NET Core 的本机会话实现。

ASP.NET Core 提供了一个轻量级、高性能的会话状态实现,它将数据存储为 byte[],并要求显式序列化。 此方法提供最佳性能,但在迁移过程中需要更多代码更改。

有关如何设置和使用它的详细信息,请参阅 [ASP.NET 会话文档](xref:fundamentals/app-state.md)。

优点和缺点

优点 缺点
最佳性能和内存占用最少 需要重写所有会话访问代码
本机 ASP.NET Core 实现 无自动对象序列化
完全控制序列化策略 无会话锁定(并发请求可能导致冲突)
没有其他依赖项 ASP.NET Framework 模式的重大更改
支持所有 ASP.NET 核心会话提供程序 会话密钥区分大小写(与 Framework 不同)

迁移注意事项

迁移到内置 ASP.NET 核心会话时:

需要更改代码:

  • Session["key"] 替换为 HttpContext.Session.GetString("key")
  • Session["key"] = value 替换为 HttpContext.Session.SetString("key", value)
  • 为复杂对象添加显式序列化/反序列化
  • 显式处理 null 值(无自动类型转换)

数据迁移:

  • 会话数据结构更改需要仔细规划
  • 考虑在迁移期间并行运行这两个系统
  • 根据需要实现会话数据导入/导出实用工具

测试策略:

  • 单元测试会话的序列化/反序列化逻辑
  • 跨请求的集成测试会话行为
  • 负载测试并发会话访问模式

何时选择此方法:

  • 可以负担得起重写与会话相关的代码
  • 性能是首要任务
  • 你未与旧版应用程序共享会话数据
  • 想要完全消除 System.Web 依赖项

System.Web 适配器会话

注释

这利用 System.Web 适配器 来简化迁移。

序列化配置

HttpSessionState 对象需要远程应用会话状态的序列化。

在 ASP.NET Framework 中, BinaryFormatter 用于自动序列化会话值内容。 为了将这些内容序列化,以便与 System.Web 适配器一起使用,序列化必须使用 ISessionKeySerializer 实现显式配置。

开箱即用,有一个简单的 JSON 序列化程序,允许使用 JsonSessionSerializerOptions 将每个会话密钥注册到已知类型:

  • RegisterKey<T>(string) - 注册会话密钥为已知类型。 此注册对于正确的序列化/反序列化是必需的。 缺少注册会导致错误并阻止会话访问。
builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // Serialization/deserialization requires each session key to be registered to a type
        options.RegisterKey<int>("test-value");
    });

如果需要更多自定义项,则可以实现 ISessionKeySerializer

builder.Services.AddSystemWebAdapters()
    .AddSessionSerializer();

builder.Services.AddSingleton<ISessionKeySerializer, CustomSessionKeySerializer>();
sealed class CustomSessionKeySerializer : ISessionKeySerializer
{
    public bool TryDeserialize(string key, byte[] bytes, out object? obj)
    {
        // Custom deserialization logic
    }

    public bool TrySerialize(string key, object? value, out byte[] bytes)
    {
        // Custom serialization logic
    }
}

注释

使用 AddJsonSessionSerializer 注册模式时,无需调用 AddSessionSerializer ,因为它会自动添加。 如果只想使用自定义实现,则必须手动添加它。

启用会话

会话支持需要显式激活。 使用 ASP.NET 核心元数据按路由或全局配置它:

  • 批注控制器
[Session]
public class SomeController : Controller
{
}
  • 在所有终结点上启用全局功能
app.MapDefaultControllerRoute()
    .RequireSystemWebAdapterSession();

包装过的 ASP.NET Core 会话状态

如果迁移的组件不需要与旧应用程序共享会话数据,请选择此方法。

Microsoft.Extensions.DependencyInjection.WrappedSessionExtensions.AddWrappedAspNetCoreSession 扩展方法添加了一个包装的 ASP.NET Core 会话,与适配器一起使用。 它使用与 ISession 相同的后备存储,同时提供强类型访问。

ASP.NET Core 的配置:

builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // Serialization/deserialization requires each session key to be registered to a type
        options.RegisterKey<int>("test-value");
        options.RegisterKey<SessionDemoModel>("SampleSessionItem");
    })
    .AddWrappedAspNetCoreSession();

框架应用程序无需进行任何更改。

有关详细信息,请参阅 封装的会话状态示例应用

远程应用会话状态

注释

这利用 System.Web 适配器 来简化迁移。

如果需要在增量迁移期间在 ASP.NET Framework 和 ASP.NET Core 应用程序之间共享会话状态,请选择此方法。

远程应用会话通过公开 ASP.NET Framework 应用上的终结点,使应用程序之间的通信能够检索和设置会话状态。

先决条件

完成 远程应用设置 说明,以连接 ASP.NET Core 和 ASP.NET Framework 应用程序。

应用程序配置

ASP.NET 核心配置:

调用 AddRemoteAppSessionAddJsonSessionSerializer 注册已知会话项类型:

builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // Serialization/deserialization requires each session key to be registered to a type
        options.RegisterKey<int>("test-value");
        options.RegisterKey<SessionDemoModel>("SampleSessionItem");
    })
    .AddRemoteAppClient(options =>
    {
        // Provide the URL for the remote app that has enabled session querying
        options.RemoteAppUrl = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);

        // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session
        options.ApiKey = builder.Configuration["RemoteAppApiKey"];
    })
    .AddSessionClient();

ASP.NET 框架配置:

将此更改添加到 Global.asax.cs

public class Global : HttpApplication
{
        protected void Application_Start()
        {
            SystemWebAdapterConfiguration.AddSystemWebAdapters(this)
                .AddJsonSessionSerializer(options =>
                {
                    // Serialization/deserialization requires each session key to be registered to a type
                    options.RegisterKey<int>("test-value");
                    options.RegisterKey<SessionDemoModel>("SampleSessionItem");
                })
                // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session
                // ApiKey is a string representing a GUID
                .AddRemoteAppServer(options => options.ApiKey = ConfigurationManager.AppSettings["RemoteAppApiKey"])
                .AddSessionServer();
        }
}

使用 Aspire 时,配置将通过环境变量完成,并由 AppHost 设置。 若要启用远程会话,必须启用该选项:

...

var coreApp = builder.AddProject<Projects.CoreApplication>("core")
    .WithHttpHealthCheck()
    .WaitFor(frameworkApp)
    .WithIncrementalMigrationFallback(frameworkApp, options => options.RemoteSession = RemoteSession.Enabled);

...

通信协议

只读会话

只读会话在不锁定的情况下检索会话状态。 此过程使用返回会话状态并立即关闭的单个 GET 请求。

只读会话将从框架应用检索会话状态

可写会话

可写入的会话需要附加步骤:

  • 从与只读会话相同的 GET 请求开始
  • 使初始 GET 请求保持打开状态,直到会话完成
  • 使用额外的 PUT 请求更新状态
  • 仅在更新完成后关闭初始请求

可写会话状态协议的开头与只读协议相同