第三单元 管道与中间件

发布时间 2023-12-06 11:54:38作者: 誉尚学教育

1. 什么是中间件

在ASP.NET Core中,中间件(Middleware)是一个可以处理HTTP请求或响应的软件管道。 ASP.NET Core中给中间件组件的定位是具有非常特定的用途。例如,我们可能有需要一个中间件组件验证用户,另一个中间件来处理错误,另一个中间件来提供静态文件,如JavaScript文件,CSS文件,图片等等。

中间件就是用于组成应用程序管道来处理请求和响应的组件 。

中间件可以认为有两个基本的职责:

  1. 选择是否将请求传递给管道中的下一个中间件。

  2. 可以在管道中的下一个中间件前后执行一些工作。

我们使用这些中间件组件在ASP.NET Core中设置请求处理管道,而正是这管道决定了如何处理请求。 而请求管道是由Startup.cs文件中的Configure()方法进行配置,它也是应用程序启动的一个重要部分。

// 配置http 请求管道,由运行时调用
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage(); // 渲染错误页中间件
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }
    app.UseStaticFiles(); // 使用静态文件中间件
    
    app.UseRouting(); // 使用路由中间件
​
    app.UseAuthorization(); // 使用授权中间件
    // 终端节点中间件
    app.UseEndpoints(endpoints =>
                     {
                         endpoints.MapControllerRoute(
                             name: "default",
                             pattern: "{controller=Home}/{action=Index}/{id?}");
                     });
}
 

 

2. 中间件处理流程-请求管道

.Net Core管道(pipeline)是什么?

简单来说,就是从发起请求到返回结果的一个过程,在.Net Core中这里面的处理是由中间件(middleware)来完成。 管道机制解释 用户在发起请求后,系统会自动生成一个请求管道(request pipeline),在这个请求管道中,可以通过run、map和use方法来配置请求委托(RequestDelegate),而在单独的请求委托中定义的可重用的类和并行的匿名方法即为中间件,也叫做中间件组件。当发起请求后,系统会创建一个请求管道,在这个管道中,每一个中间件都会按顺序处理(可能会执行,也可能不会被执行,取决于具体的业务逻辑),等最后一个中间件处理完后,又会按照相反的方向返回最终的处理结果。

例如,如果您有一个日志记录中间件,它可能只是记录请求的时间,它处理完毕后将请求传递给下一个中间件以进行进一步处理。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(); // 添加控制器服务
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILogger<Startup> logger)
{
    app.UseRouting();// 添加路由服务

    app.Use(next =>
            {
                logger.LogInformation("第1个中间件"); // 启动时执行,只执行一次
                
                // 每次请求都会执行一次
                return async context =>
                {
                    logger.LogInformation("第1个中间件1-before");
                    await next(context);
                    logger.LogInformation("第1个中间件1-after");
                };
            });

    app.Use(next =>
            {
                logger.LogInformation("第2个中间件");// 启动时执行,只执行一次

                 // 每次请求都会执行一次
                return async context =>
                {
                    logger.LogInformation("第2个中间件2-before");
                    await next(context);
                    logger.LogInformation("第2个中间件2-after");
                };
            });

    app.Use(next =>
            {
                logger.LogInformation("第3个中间件");// 启动时执行,只执行一次

                 // 每次请求都会执行一次
                return async context =>
                {
                    logger.LogInformation("第3个中间件3-before");
                    await next(context);
                    logger.LogInformation("第3个中间件3-after");
                };
            });
    
    // 使用终端节点中间件会短路后面的中件间,所以,这个中间件最好放在最后
    app.UseEndpoints(endpoints =>
                     {
                         endpoints.MapControllerRoute(
                             name: "default",
                             pattern: "{controller=Home}/{action=Index}/{id?}");
                     });
}
info: Step3.Empty.Startup[0]
      第3个中间件
info: Step3.Empty.Startup[0]
      第2个中间件
info: Step3.Empty.Startup[0]
      第1个中间件

// 发起请求之后
info: Step3.Empty.Startup[0]
      第1个中间件1-before
info: Step3.Empty.Startup[0]
      第2个中间件2-before
info: Step3.Empty.Startup[0]
      第3个中间件3-before
info: Step3.Empty.Startup[0]
      第3个中间件3-after
info: Step3.Empty.Startup[0]
      第2个中间件2-after
info: Step3.Empty.Startup[0]
      第1个中间件1-after

 

中间件顺序

下图显示了 ASP.NET Core MVC 和 Razor Pages 应用的完整请求处理管道。 你可以在典型应用中了解现有中间件的顺序,以及在哪里添加自定义中间件。 你可以完全控制如何重新排列现有中间件,或根据场景需要注入新的自定义中间件。

 

3. 什么是短路

中间件组件可以处理请求, 并决定不调用管道中的下一个中间件,从而使管道短路,官方微软给了一个英文的名字叫“terminal middleware ”,翻译为“终端中间件”。短路通常是被允许的,因为它可以避免一些不必要的工作。 例如, 如果请求的是像图像或 css 文件这样的静态文件, 则 StaticFiles 中间件可以处理和服务该请求并使管道中的其余部分短路。这个意思就是说,在我们的示例中, 如果请求是针对静态文件, 则 Staticile 中间件不会调用 MVC 中间件,避免一些无谓的操作。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env,ILogger<Startup> logger)
{
    app.UseRouting();

    app.Use(next =>
            {
                logger.LogInformation("第1个中间件");

                return async context =>
                {
                    logger.LogInformation("第1个中间件1-before");
                    await next(context);
                    logger.LogInformation("第1个中间件1-after");
                };
            });

    // 后面的中件间将不会再执行了
    app.Use(next =>
            {
                logger.LogInformation("第2个中间件");

                return async context =>
                {
                    logger.LogInformation("短路了");
                    // await next(context); // 没有调用即表示短路了
                };
            });
    
    app.Use(next =>
            {
                logger.LogInformation("第3个中间件");

                return async context =>
                {
                    logger.LogInformation("第3个中间件3-before");
                    await next(context);
                    logger.LogInformation("第3个中间件3-after");
                };
            });

    app.UseEndpoints(endpoints =>
                     {
                         endpoints.MapControllerRoute(
                             name: "default",
                             pattern: "{controller=Home}/{action=Index}/{id?}");
                     });
}
输出结果:

info: Step3.Empty.Startup[0]
      第3个中间件
info: Step3.Empty.Startup[0]
      第2个中间件
info: Step3.Empty.Startup[0]
      第1个中间件

// 发起请求之后
info: Step3.Empty.Startup[0]
      第1个中间件1-before
info: Step3.Empty.Startup[0]
      短路了
info: Step3.Empty.Startup[0]
      第1个中间件1-after

 

app.Use 与 app.Run 的区别

它俩都可以添加一个中间件至请求管道中。

  1. Use 有权决定是否执行下一个中间件,如果不执行,则出现短路情况

  2. Run 是直接短路,不会执行后面的中间件。

 

4. 常用的系统中间件

1. 路由中间件

ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。 若要设置路由模板,则必须执行添加如下中间件至执行管道.

// 添加控制器与视图服务
builder.Services.AddControllersWithViews();


// ....上面省略一些代码
app.UseRouting();

 

路由模板:

  • 在启动时 Program.cs 或在属性中定义。

  • 描述 URL 路径如何与操作相匹配。

  • 用于生成链接的 URL。 生成的链接通常在响应中返回。

操作既支持传统路由,也支持属性路由。 通过在控制器或操作上放置路由可实现属性路由。 有关详细信息,请参阅混合路由

路由模板示例匹配 URI请求 URI…
hello /hello 仅匹配单个路径 /hello
{Page=Home} / 匹配并将 Page 设置为 Home
{Page=Home} /Contact 匹配并将 Page 设置为 Contact
{controller}/{action}/{id?} /Products/List 映射到 Products 控制器和 List 操作。
{controller}/{action}/{id?} /Products/Details/123 映射到 Products 控制器和 Details 操作,并将 id 设置为 123。
{controller=Home}/{action=Index}/{id?} / 映射到 Home 控制器和 Index 方法。 id 将被忽略。
{controller=Home}/{action=Index}/{id?} /Products 映射到 Products 控制器和 Index 方法。 id 将被忽略。

设置传统路由

app.UseRouting();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run(); // 必须添加一个终节点
路由模板 "{controller=Home}/{action=Index}/{id?}":

匹配 URL 路径,例如 /Products/Details/5

通过标记路径来提取路由值 { controller = Products, action = Details, id = 5 }。 如果应用有一个名为 ProductsController 的控制器和一个 Details 操作,则提取路由值会导致匹配:

public class ProductsController : Controller
{
    public IActionResult Details(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
MyDisplayRouteInfo 由 Rick.Docs.Samples.RouteInfo NuGet 包提供,会显示路由信息。

/Products/Details/5 模型绑定 id = 5 的值,以将 id 参数设置为 5。 有关更多详细信息,请参阅模型绑定。

{controller=Home} 将 Home 定义为默认 controller。

{action=Index} 将 Index 定义为默认 action。

{id?} 中的 ? 字符将 id 定义为可选。

默认路由参数和可选路由参数不必包含在 URL 路径中进行匹配。 有关路由模板语法的详细说明,请参阅路由模板参考。

匹配 URL 路径 /。

生成路由值 { controller = Home, action = Index }。

controller 和 action 的值使用默认值。 id 不会生成值,因为 URL 路径中没有相应的段。 / 仅在存在 HomeController 和 Index 操作时匹配:

public class HomeController : Controller
{
    public IActionResult Index() { ... }
}
使用前面的控制器定义和路由模板,为以下 URL 路径运行 HomeController.Index 操作:

/Home/Index/17

/Home/Index

/Home

/

URL 路径 / 使用路由模板默认 Home 控制器和 Index 操作。 URL 路径 /Home 使用路由模板默认 Index 操作。

简便方法 MapDefaultControllerRoute:

app.MapDefaultControllerRoute();
替代:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
 

属性路由
// 添加控制器与视图服务
builder.Services.AddControllersWithViews();


// ....上面省略一些代码 
// app.UseRouting(); // 此行代码已经不需要了
app.MapControllers(); // 映射控制器
MapControllers 调用它来映射属性路由控制器。

在以下示例中:

HomeController 匹配一组类似于默认传统路由 {controller=Home}/{action=Index}/{id?} 匹配的 URL。

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
将针对任意 URL 路径 /、/Home、/Home/Index 或 /Home/Index/3 执行 HomeController.Index 操作。

此示例重点介绍属性路由与传统路由之间的主要编程差异。 属性路由需要更多输入才能指定路由。 传统默认路由会更简洁地处理路由。 但是,属性路由允许并需要精确控制应用于每项操作的路由模板。

对于属性路由,控制器和操作名称在操作匹配中不起作用,除非使用标记替换。 以下示例匹配与上一个示例相同的 URL:

public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
以下代码对 action 和 controller 使用标记替换:

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("[controller]/[action]")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("[controller]/[action]")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}
以下代码将 [Route("[controller]/[action]")] 应用于控制器:

[Route("[controller]/[action]")]
public class HomeController : Controller
{
    [Route("~/")]
    [Route("/Home")]
    [Route("~/Home/Index")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

 

在前面的代码中,Index 方法模板必须将 /~/ 预置到路由模板。 应用于操作的以 /~/ 开头的路由模板不与应用于控制器的路由模板合并。

有关路由模板选择的信息,请参阅路由模板优先级

 

路由约束(可选)

路由约束在传入 URL 发生匹配时执行,URL 路径标记为路由值。 路径约束通常检查通过路径模板关联的路径值,并对该值是否为可接受做出对/错决定。 某些路由约束使用路由值以外的数据来考虑是否可以路由请求。 例如,HttpMethodRouteConstraint 可以根据其 HTTP 谓词接受或拒绝请求。 约束用于路由请求和链接生成。

警告

请勿将约束用于输入验证。 如果约束用于输入验证,则无效的输入将导致 404(找不到页面)响应。 无效输入可能生成包含相应错误消息的 400 错误请求。 路由约束用于消除类似路由的歧义,而不是验证特定路由的输入。

下表演示示例路由约束及其预期行为:

约束示例匹配项示例说明
int {id:int} 123456789, -123456789 匹配任何整数
bool {active:bool} true, FALSE 匹配 truefalse。 不区分大小写
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm 在固定区域性中匹配有效的 DateTime 值。 请参阅前面的警告。
decimal {price:decimal} 49.99, -1,000.01 在固定区域性中匹配有效的 decimal 值。 请参阅前面的警告。
double {weight:double} 1.234, -1,001.01e8 在固定区域性中匹配有效的 double 值。 请参阅前面的警告。
float {weight:float} 1.234, -1,001.01e8 在固定区域性中匹配有效的 float 值。 请参阅前面的警告。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 匹配有效的 Guid
long {ticks:long} 123456789, -123456789 匹配有效的 long
minlength(value) {username:minlength(4)} Rick 字符串必须至少为 4 个字符
maxlength(value) {filename:maxlength(8)} MyFile 字符串不得超过 8 个字符
length(length) {filename:length(12)} somefile.txt 字符串必须正好为 12 个字符
length(min,max) {filename:length(8,16)} somefile.txt 字符串必须至少为 8 个字符,且不得超过 16 个字符
min(value) {age:min(18)} 19 整数值必须至少为 18
max(value) {age:max(120)} 91 整数值不得超过 120
range(min,max) {age:range(18,120)} 91 整数值必须至少为 18,且不得超过 120
alpha {name:alpha} Rick 字符串必须由一个或多个字母字符组成,a-z,并区分大小写。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 字符串必须与正则表达式匹配。 请参阅有关定义正则表达式的提示。
required {name:required} Rick 用于强制在 URL 生成过程中存在非参数值

警告

如果使用 System.Text.RegularExpressions 处理不受信任的输入,则传递一个超时。 恶意用户可能会向 RegularExpressions 提供输入,从而导致拒绝服务攻击。 使用 RegularExpressions 的 ASP.NET Core 框架 API 会传递一个超时。

 

可向单个参数应用多个用冒号分隔的约束。 例如,以下约束将参数限制为大于或等于 1 的整数值:

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }

 

警告

验证 URL 的路由约束并将转换为始终使用固定区域性的 CLR 类型。 例如,转换为 CLR 类型 intDateTime。 这些约束假定 URL 不可本地化。 框架提供的路由约束不会修改存储于路由值中的值。 从 URL 中分析的所有路由值都将存储为字符串。 例如,float 约束会尝试将路由值转换为浮点数,但转换后的值仅用来验证其是否可转换为浮点数。

 

疑惑解答:

1. 当访问一个Web 应用地址时,Asp.Net Core 是怎么执行到ControllerAction的呢?

答:程序启动的时候会把所有的Controller 中的Action 映射存储到routeOptions 的集合中,Action 映射成Endpoint终结者 的RequestDelegate 委托属性,最后通过UseEndPoints 添加EndpointMiddleware 中间件进行执行,同时这个中间件中的Endpoint 终结者路由已经是通过Rouing匹配后的路由。

2. EndPoint 跟普通路由又存在着什么样的关系?

答:Ednpoint 终结者路由是普通路由map 转换后的委托路由,里面包含了路由方法的所有元素信息EndpointMetadataCollectionRequestDelegate 委托。

3. UseRouing()UseAuthorization()UseEndpoints() 这三个中间件的关系是什么呢?

答:UseRouing 中间件主要是路由匹配,找到匹配的终结者路由EndpointUseEndpoints 中间件主要针对UseRouing 中间件匹配到的路由进行 委托方法的执行等操作。 UseAuthorization 中间件主要针对 UseRouing 中间件中匹配到的路由进行拦截 做授权验证操作等,通过则执行下一个中间件UseEndpoints(),具体的关系可以看下面的流程图:

 

上面流程图中省略了一些部分,主要是把UseRouing 、UseAuthorization 、UseEndpoint 这三个中间件的关系突显出来。

 

 

2. 异常中间件

UseExceptionHandler : 将中间件添加到管道,该中间件将捕获异常,记录异常,并在备用管道中重新执行请求。如果响应已启动,则不会重新执行请求。

UseDeveloperExceptionPage: 从管道捕获同步和异步异常实例,并生成 HTML 错误响应。

if (!app.Environment.IsDevelopment()) // 非开发环境下,可以显示自定义错误页
{
    app.UseExceptionHandler("/Home/Error");
}
else
{
    app.UseDeveloperExceptionPage(); // 开发人员错误页
}

 

 

3. 静态资源中间件

默认情况下,静态文件(如 HTML、CSS、图像和 JavaScript)是 ASP.NET Core 应用直接提供给客户端的资产。

静态文件存储在项目的 Web 根目录中。 默认目录为 {content root}/wwwroot,但可通过 UseWebRoot 方法更改目录。 有关详细信息,请参阅内容根目录Web 根目录

Web 应用程序项目模板包含 wwwroot 文件夹中的多个文件夹:

  • wwwroot

    • css 样式文件

    • js 脚本文件

    • lib 第三方前端库

    • images 图片文件

 

默认 Web 应用模板在 Program.cs 中调用 UseStaticFiles 方法,这将允许提供静态文件:

app.UseStaticFiles();
以下标记引用 wwwroot/images/MyImage.jpg:

<img src="~/images/MyImage.jpg" class="img" alt="My image" />

 

提供wwwroot 根目录外的文件

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
           Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/StaticFiles" // 请求路径
});

 

目录结果变成如下(wwwroot 与 MyStaticFiles都可以存放静态资源文件):

  • wwwroot

    • css

    • images

    • js

  • MyStaticFiles

    • images

      • red-rose.jpg

请求方式:

<img src="~/StaticFiles/images/red-rose.jpg" class="img" alt="A red rose" />

 

 

4. Session中间件

会话是指浏览器打开到关闭的过程中,多次与服务器发送接收数据的过程。

由于HTTP是无状态协议,一次请求响应过后,产生的数据就随之释放了,可是在某些情况下,我们希望服务器保存我们的一些数据,方便下次请求(比如网站的账户登录信息,等等)。如果要保存这些发送中的数据,就要用到会话技术(Cookie技术本节不涉及),服务器会将每个浏览器的单独标识,将每个浏览器需要保存的数据,保存下来,当下次需要这些保存的数据,就可以取出来用。

正式点说,会话技术(Session)服务器端保存浏览器请求数据的一项技术,数据是以键值对的形式保存到服务器内存中,可以解决无状态协议带来的弊端,减少每次请求的数据量,提高了性能。

builder.Services.AddSession(); // 添加Session服务


app.UseStaticFiles();

app.UseSession(); // 添加Session中间件,使用默认的SessionOptions
app.UseRouting();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Hello}/{action=Index}/{id?}");


app.Run();

 

SessionOptions:

Cookie确定用于创建 Cookie 的设置。Name 默认为 CookieNamePath 默认为 CookiePathSameSite 默认为 LaxHttpOnly defaults to trueIsEssential defaults to false
IdleTimeout IdleTimeout 指示会话在放弃内容之前可以空闲多长时间。 每个会话访问都会重置超时。 请注意,这仅适用于会话内容,不适用于 Cookie。
IOTimeout 允许从存储加载会话或者将其提交回存储的最大时长。 请注意,这可能仅适用于异步操作。 可以使用 InfiniteTimeSpan 来禁用此超时。
builder.Services.AddSession(options =>
            {   
                // 默认名称.AspNetCore.Session
                options.Cookie.Name = ".AdventureWorks.Session"; 
                // 默认20分钟过期
                options.IdleTimeout = TimeSpan.FromSeconds(2000);//设置session的过期时间
                // 默认为真
                options.Cookie.HttpOnly = true;//设置在浏览器不能通过js获得该cookie的值 
            });

 

1. ISession 接口

在用户浏览 Web 应用程序时存储用户数据。 会话状态使用应用程序维护的存储来跨来自客户端的请求保留数据。 会话数据由缓存提供支持,并被视为临时数据。

派生 了Microsoft.AspNetCore.Session.DistributedSession

属性

Id当前会话的唯一标识符。 这与会话 Cookie 不同,因为 Cookie 生存期可能与数据存储中的会话条目生存期不同。
IsAvailable 指示当前会话是否已成功加载。 在加载会话之前访问此属性将导致其内联加载。
Keys 枚举所有键(如果有)。

方法

Clear()从当前会话中删除所有条目(如果有)。 不会删除会话 Cookie。
CommitAsync(CancellationToken) 将会话存储在数据存储中。 如果数据存储不可用,可能会引发此问题。
LoadAsync(CancellationToken) 从数据存储加载会话。 如果数据存储不可用,可能会引发此问题。
Remove(String) 如果存在,请从会话中删除给定密钥。
Set(String, Byte[]) 在当前会话中设置给定的键和值。 如果在发送响应之前未建立会话,则会引发此事件。
TryGetValue(String, Byte[]) 检索给定键的值(如果存在)。

扩展方法

Get(ISession, String)ISession中获取字节数组值。
GetInt32(ISession, String) ISession中获取 int 值。
GetString(ISession, String) ISession中获取字符串值。
SetInt32(ISession, String, Int32) 在 . 中 ISession设置 int 值。
SetString(ISession, String, String) 在 . 中ISession设置一个String

2. 在控制器中使用:

HttpContext.Session.SetString("user","任我行"); // 设置Session的值

HttpContext.Session.GetString("user","任我行"); // 读取Session的值
// Razor视图中读取
<h1>我的姓名:@Context.Session.GetString("user")</h1> // 读取Session的值
 

 

3. 在普通类中使用:

  1. 将IHttpContextAccessor 接口加入至 IServiceCollection 容器中

    // 单例模式注册
    builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    
    builder.Services.AddScoped<IUserService, UserService>();

     

  2. 新建一个普通接口IUserService与普通类UserService,UserService实现IUserService接口,用于测试。

    public interface IUserService
    {
        UserInfo GetById(int id);
    }
    
    public record UserInfo
    {
        public int Id { get; set; }
        public string? UserName { get; set; }
    }
    
    public class UserService:IUserService
    {
        private readonly ISession _session;
    
        public UserService(IHttpContextAccessor httpContext)
        {
            _session = httpContext.HttpContext.Session; // 获取Session服务
        }
    
        public UserInfo GetById(int id)
        {
            _session.SetString("userinfo",id.ToString());
            return new UserInfo {Id = 1, UserName = "任我行"};
        }
    }

     

4. 对象序列化-ProtoBuf-Net

Protocol Buffer(简称Protobuf) 是 Google 公司内部提供的数据序列化和反序列化标准,与 JSON 和 XML 格式类似,同样大小的对象,相比 XML 和 JSON 格式, Protobuf 序列化后所占用的空间最小。 Protocol Buffers 是一种轻便高效的结构化数据存储格式,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式

  1. nuget 安装 protobuf-net

  2. 如果写入的是个类,类名上要加 [ProtoContract],属性上要加 [ProtoMember(1)] [ProtoMember(2)]等等

    [ProtoContract]
    public record UserInfo
    {
        [ProtoMember(1)]
        public int Id { get; set; }
        [ProtoMember(2)]
        public string? UserName { get; set; }
    }

     

  3. Session 扩展方法

    /// <summary>
    /// Session 扩展类
    /// </summary>
    public static class SessionExtensions
    {
        public static T? Get<T>(this ISession session, string key)
        {
            var buffer = session.Get(key);
            if (buffer == null)
            {
                return default;
            }
            using MemoryStream ms = new MemoryStream(buffer);
            return Serializer.Deserialize<T>(ms); // ProtoBuf 反序列化
        }
    
        public static void Set<T>(this ISession session, string key, T obj)
        {
            if (obj == null)
            {
                return;
            }
            using MemoryStream ms = new MemoryStream();
            Serializer.Serialize(ms,obj); // ProtoBuf 序列化
            session.Set(key,ms.ToArray());
        }
    }

     

  4. 使用Session扩展

    public class UserService:IUserService
    {
        private readonly ISession _session;
    ​
        public UserService(IHttpContextAccessor httpContext)
        {
            _session = httpContext.HttpContext.Session;
        }
    ​
        public UserInfo GetById(int id)
        {
            string key = $"userinfo_{id}";
            var userInfo = _session.Get<UserInfo>(key);  // 使用扩展Get方法
            if (userInfo == null)
            {
                userInfo = new UserInfo {Id = 1, UserName = "任我行"};
                _session.Set($"userinfo_{id}",userInfo); // 使用扩展的Set方法
            }
            return userInfo;
        }
    }
    ​
    ​
    // **********************HomeController.cs 代码**********************
    private readonly IUserService _userService;
    public HomeController(IUserService userService)
    {
        _userService = userService;
    }
    ​
    ​
    public IActionResult Index()
    {
        var user =  _userService.GetById(1);
        return View();
    }
     

     

配套视频链接:【Asp.Net Core Mvc 进阶】.Net 6 系列 详细讲解,Mvc进阶讲解(已完结)_哔哩哔哩_bilibili