ASP.NET Core 本身已经内置了一系列服务和中间件组件,提供开发 Web 应用中通常的开发需求。熟悉和掌握这些特性工具,绝对让我们节省时间,提供效率。有些功能需求如果让我们自己实现的话,绞尽脑汁都可能做不完全,结果发现,用几个语句应用某个特性就万事大吉了。

准备我们的应用:
Listing 14-1 新建 NET 8 项目

dotnet new globaljson --sdk-version 8.0.400 --output ASPNETCoreFeatures
dotnet new web --output ASPNETCoreFeatures --framework8.0
dotnet enw sln --output ASPNETCoreFeatures
dotnet sln ASPNETCoreFeatures add ASPNETCoreFeatures

用 VS Code 打开项目文件或者目录。修改 Properties 文件夹里的 launchSettings.json 文件中的 http 端口为 5000, https 端口为 5001.

使用配置服务

在项目文件夹里有两个 appsettings Json 文件,它们是 dotnet new 模板创建的主配置文件。appsettings.json 是如下内容:

Listing 14-2 appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

这个文件包含两个部分。

  1. Logging 部分 这部分是配置应用的 logging 行为。

    • LogLevel :定义最小的 log 级别。


      • Default: “Information”
        表示所有严重程度小于等于 Information 的 log ,包括 Warning, Error 或者 Critical 都会被记录。低级别的 Debug 或者 Trace 将被忽略。



      • Microsoft.AspNetCore: “Warning”
        表示由 Microsoft.AspNetCore 命名空间产生的可以被记录的 log 最低严重级别是 Warning ,因此,Error 和 Critical 也将被记录。



  2. AllowedHosts 部分 指定应用可以接受从任何主机名发来的请求,也可以指定类似 “www.example.com” 这样的限定主机名。


而 appsettings.Development.json 的内容是这样的:

Listing 14-3 appsettings.Development.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

这些配置只适用开发环境,由全局变量 ASPNETCORE_ENVIRONMENT 来设定是否开发模式还是生成模式。我们经常会改变记录级别,以获得更多的信息或者帮助调试。在 launchSettings.json 文件里模板初始指定用的环境变量值是 Development ,因此这是现在的默认配置文件。
在开发过程中,我们为了获得更多的信息,可以在这个文件里添加其他重要的记录入口,比如: System 。
Listing 14-5 修改 appsettings.Development.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "System": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

访问配置

配置数据可以通过 IConfiguration 接口提供。我们通过接口的 API 来浏览配置的机构和读取配置设置。

Listing 14-6 在 Program.cs 文件里添加读取配置

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("config", async (HttpContext context, IConfiguration config) =>
{
    string? defaultLogLevel = config["Logging:LogLevel:Default"];
    await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
});


app.MapGet("/", () => "Hello World!");

app.Run();

我们用带冒号 : 的区域分隔字符串来指定路径。运行应用,在浏览器地址栏的 localhost:5000 后面加上 /config ,得到输出结果 Default log level: Information

介绍 launchSettings.json

launchSettings.json 也是一个配置文件,包含应用的启动配置设置,HTTP 和 HTTPS 请求端口等。

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:5000",
      "sslPort": 44381
    }
  },
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

IIS 配置设置服务是用于旧版的应用部署。环境变量集合里指定了一个环境变量 ASPNETCORE_ENVIRONMENT,其值为 Development 。这个变量也可以通过 IConfiguration 接口访问;如果我们在 Program.cs 文件里的 “config” 端点方法里添加访问 config[“ASPNETCORE_ENVIRONMENT”] 环境变量的语句:

Listing 14-7 修改 Program.cs

...
app.MapGet("config", async (HttpContext context, IConfiguration config) =>
{
    string? defaultLogLevel = config["Logging:LogLevel:Default"];
    await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
    string? env = config["ASPNETCORE_ENVIRONMENT"];
    await context.Response.WriteAsync($"nThe environment Setting is: {env}");
});
...

我们可以得到 The environment Setting is: Development 的输出结果。

IWebHostEnvironment 服务

对于环境变量,ASP.NET Core 提供了一个 IWebHostEnvironment 的服务,方便我们取得当前环境变量设置。这些环境变量方法都在 Microsoft.Extensions.

IWebHostEnvironment 扩展方法

方法名描述
EnvironmentName这个属性返回当前环境
IsDevelopment()如果是开发环境,此方法返回真值
IsStaging()如果是演示环境, 此方法返回真值
IsProduction()如果是生产环境,此方法返回真值
IsEnvironment(env)如果按参数定义的环境被选择,此方法返回真值

Listing 14-8 修改 Program.cs

...
app.MapGet("config", async (HttpContext context, IConfiguration config,
    IWebHostEnvironment webHostEnv) =>
{
    string? defaultLogLevel = config["Logging:LogLevel:Default"];
    await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
    string? env = config["ASPNETCORE_ENVIRONMENT"];
    await context.Response.WriteAsync($"nThe environment Setting is: {env}");
    await context.Response.WriteAsync($"nEnvName is: {webHostEnv.EnvironmentName}");
});
...

运行应用,在前面输出结果后面会增加一行 EnvName is: Development

存储用户机密数据

在开发过程中,我们经常需要用到一些比较敏感的数据。比如,API 密钥、数据库连接密码等等;如果我们把这些数据的明文都放在类或者Json 配置文件中,就有非常严重的泄露风险。因为基本上源代码库的安全性都不会特别地高,更不用提项目是做成开发源码的话。
我们可以使用用户机密服务(user secrets) ,把敏感数据存储在项目之外的一个文件里,这个文件不会被版本控制检查到,可以有效防止这些数据暴露给其他人员。

Listing 14-9 在终端中初始化用户机密

dotnet user-secrets Init

终端将显示类似的信息: “Set UserSecretsId to ‘xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx’ for MSBuild project ‘XXXXXXASPNETCoreFeaturesASPNETCoreFeatures.csproj’.” ,表示初始化成功。
接着设置我们的机密。

Listing 14-10 在终端中设置机密数据

dotnet user-secrets set "TheSecret:Id" "User"
dotnet user-secrets set "TheSecret:Password" "P@ssword"

机密数据可以用前缀:机密名 格式保存,这里我们的前缀是 TheSecret ,便于我们分组。

Listing 14-11 用 list 显示保存的机密数据

dotnet user-secrets list

前面的一番操作后,在
%APPDATA%MicrosoftUserSecrets 文件夹(或 Linux 下的
~/.microsoft/usersecrets 文件夹)中创建了一个 JSON 文件来存储机密。

Listing 14-12 修改 Program.cs 的 MapGet 方法,加入读取机密代码

...
app.MapGet("config", async (HttpContext context, IConfiguration config,
    IWebHostEnvironment webHostEnv) =>
{
    string? defaultLogLevel = config["Logging:LogLevel:Default"];
    await context.Response.WriteAsync($"Default log level: {defaultLogLevel}");
    string? env = config["ASPNETCORE_ENVIRONMENT"];
    await context.Response.WriteAsync($"nThe environment Setting is: {env}");
    await context.Response.WriteAsync($"nEnvName is: {webHostEnv.EnvironmentName}");

    string? userId = config["TheSecret:Id"];
    string? userPassword = config["TheSecret:Password"];
    await context.Response.WriteAsync($"nUser Id: {userId}");
    await context.Response.WriteAsync($"nUser Password: {userPassword}");
});
...

运行应用后,我们可以得到输出结果中显示正确的机密数据。

User Id: User
User Password: P@ssword

使用日志 Logging

ASP.NET Core 包含一个内置日志服务,使开发人员能够将不同级别(如调试、警告、错误)的信息记录到各种输出,如控制台、文件或第三方日志提供程序。
默认情况下启用三个内置提供程序:控制台提供程序、调试提供程序和事件源提供程序、调试提供程序和事件源提供程序。
其他两个常用的提供者: 文件提供程序(用第三方扩展 Serilog 或者 NLog 把日志文件记录到一个文件),Azure Monitor 整合。
调试程序提供程序提供程序转发消息,以便通过 System.Diagnostics.Debug 类处理,而事件源提供程序则转发消息到事件跟踪工具进行后续处理。

日志主要功能

  • 内置抽象 ASP.NET Core 使用 ILogger 接口来抽象日志实现。
  • 日志级别 可按不同的严重程度编写日志:
    • 跟踪: 最详细的日志,用于调试。
    • 调试: 用于开发和调试。
    • 信息: 用于一般应用程序流程跟踪。
    • 警告: 用于意外情况或可恢复的错误。
    • 错误: 用于妨碍功能的错误。
    • 严重: 用于需要立即处理的严重错误。
    • 无: 禁用日志记录。
  • 可扩展性 可使用 Serilog、NLog 或 log4net 等第三方库进行高级日志记录。
  • 结构化日志记录 日志信息可包括结构化数据(如键值对)。
  • 配置 日志设置可在 appsettings.json 中配置,也可在 appsettings.Development.json 等特定环境中重写。

使用日志记录的必要性

  • 故障排除: 帮助识别和调试生产和开发环境中的问题。
  • 监控: 深入了解应用程序的行为和性能。
  • 审计跟踪: 跟踪用户活动和系统事件。
  • 集中管理: 日志可发送到 Azure Monitor、Splunk 或 ELK 等服务进行分析。

使用日志

对于中间件 middleware 和端点 endpoint ,ASP.NET Core 默认是可以直接让它们直接存取 ILogger 。

在项目文件夹新建一个文件夹 Endpoints ,在其中新建一个 Coffee.cs 文件。

Listing 14-13 在 Endpoints 文件夹里新建 Coffee.cs 文件

namespace ASPNETCoreFeatures.Endpoints
{
    public class Coffee
    {
        public static async Task GetCoffee(HttpContext context
            , ILogger<Coffee> logger)
        {
            logger.LogDebug($"Getting coffee for {context.Request.Path}");
            string coffeeType = context.Request.RouteValues["type"] as string
                ?? "espresso";
            int calorie = coffeeType.ToLower() switch
            {
                "espresso" => 5,
                "latte" => 190,
                "cappuccino" => 120,
                _ => 0
            };

            await context.Response.WriteAsync($"Your {coffeeType} has {calorie} calories.");
            logger.LogDebug($"Coffee is ready from {context.Request.Path}");
        }
    }
}

我们先给端点一个 ILogger<Coffee> 参数,因为日志信息使用 ILogger<T> 接口;接着在这个端点类的方法的开始和结束前各添加一条调用 LogDebug 的语句。日志使用的约定是用类来特指泛型 T 代表的日志类别。中间件和端点可以不用构造函数,直接默认已经依赖注入了 ILogger 接口。在方法作用,用 LogDebug 方法来生成日志。

ILogger<T> 接口具有的扩展方法:

  • LogTrace 用于在开发中跟踪低级别调试
  • LogDebug 用于在开发中或者生产版中低级别调式
  • LogInformation 提供应用的通用状态信息
  • LogError 用于记录被应用处理的异常或者错误
    _ LogCritcal 用于记录严重错误

接着,我们在 Program.cs 中添加端点方法。
Listing 14-14 修改 Program.cs

using ASPNETCoreFeatures.Endpoints;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("coffee/{type?}", Coffee.GetCoffee);

app.MapGet("/", () => "Hello World!");

app.Run();

在终端里输入 dotnet run 运行应用,在浏览器地址栏主地址后面加上 /coffee/latte ,调用端点方法,页面会显示:

Your latte has 190 calories.

但是,日志信息在哪里呢? 终端没有输入我们设定的信息。是我们的代码错误?不是!终端没有显示日志信息的原因是开发环境配置文件 appsettings.Development.json 的 Logging 项里 Default 的值是 Information ,不能显示低级的调试信息。
Listing 14-15 修改 appsettings.Development.json 内容

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

重新启动应用,输入 /coffee/latte 回车,在终端会显示类似信息:

dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
      Hosting started
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
      Getting coffee for /coffee/latte
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
      Coffee is ready from /coffee/latte

我们可以看到这两条日志分类是 ASPNETCoreFeatures.Endpoints.Coffee 。

在主入口里使用日志

如果我们把主入口改成这样:
Listing 14-16 给 Program.cs 添加日志

using ASPNETCoreFeatures.Endpoints;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Logger.LogDebug("Starting the pipeline configuration ... ");
app.MapGet("coffee/{type?}", Coffee.GetCoffee);

app.MapGet("/", () => "Hello World!");
app.Logger.LogDebug("Pipeline configuration is done.");

app.Run();

在终端运行 dotnet run 命令,输出内容类似:

dbug: ASPNETCoreFeatures[0]
      Starting the pipeline configuration ...
dbug: ASPNETCoreFeatures[0]
      Pipeline configuration is done.
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
      Hosting starting
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000

这两条日志的分类是项目名称 ASPNETCoreFeatures ,因为 ILogger 是属于 WebApplication 类的。我们也可以直接在主入口创建自定义日志分类。

Listing 14-17 修改 Program.cs ,创建自定义日志分类

using ASPNETCoreFeatures.Endpoints;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var logger = app.Services.GetRequiredService<ILoggerFactory>()
    .CreateLogger("Coffee");

logger.LogDebug("Starting the pipeline configuration ... ");
app.MapGet("coffee/{type?}", Coffee.GetCoffee);

app.MapGet("/", () => "Hello World!");
logger.LogDebug("Pipeline configuration is done.");

app.Run();

我们使用 ILoggerFactory 接口来创建自定义日志分类 Coffee 。运行应用,终端输出类似内容:

dbug: Coffee[0]
      Starting the pipeline configuration ...
dbug: Coffee[0]
      Pipeline configuration is done.
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
      Hosting starting

日志文件的分类改为了 Coffee 。

HTTP 请求和响应日志

ASP.NET Core 内建了一个记录 HTTP 日志的中间件,可以显示管道中详细的请求和响应信息。不过,在开发中,我们可能会更自然的查看浏览器的 Inspect 开发信息,更有条理也更详细。

Listing 14-18 在 appsettings.Development.json 文件添加 HttpLogging 入口

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
    }
  }
}

在主入口添加 HTTP Logging 服务。

Listing 14-19 在 Program.cs 中取消 logger,添加 HttpLogging 服务

using ASPNETCoreFeatures.Endpoints;
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(static options => {
    options.LoggingFields = HttpLoggingFields.RequestProperties
        | HttpLoggingFields.ResponsePropertiesAndHeaders
        | HttpLoggingFields.ResponseStatusCode;
});

var app = builder.Build();

app.UseHttpLogging();

app.MapGet("coffee/{type?}", Coffee.GetCoffee);

app.MapGet("/", () => "Hello World!");

app.Run();

运行应用,输入应用地址加 /coffee/latte ,终端窗口将显示 Http 日志:

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Protocol: HTTP/1.1
      Method: GET
      Scheme: http
      PathBase:
      Path: /coffee/latte
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
      Getting coffee for /coffee/latte
dbug: ASPNETCoreFeatures.Endpoints.Coffee[0]
      Coffee is ready from /coffee/latte
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Date: Sat, 18 Jan 2025 12:28:19 GMT
      Server: Kestrel
      Transfer-Encoding: chunked

适用静态内容

大部分的应用都会是静态内容和动态内容的一个混合体。动态内容基本上会和后端服务已经数据发生交互后,在客户端更新内容。静态内容会是一些常用的,内容保持不变的轻量级网页。ASP.NET Core 的 Web 应用里,按约定,静态内容会保持在 wwwroot 文件夹下。dotnet new web 模板没有添加这个文件夹,我们可以手动添加一个 wwwroot 文件夹。

Listing 14-20 在 wwwroot 文件夹里添加一个 Index.html 文件

<!DOCTYPE html>

<head>
    <title>Index-wwwroot</title>
</head>

<body>
    <h3>Hello World!</h3>
</body>

</html>

为了显示静态内容,我们需要在主入口添加静态内容中间件。

Listing 14-21 在 Program.cs 文件中添加静态内容中间件

...
app.UseHttpLogging();

app.UseStaticFiles();

app.MapGet("coffee/{type?}", Coffee.GetCoffee);
...

运行应用,主地址里得到的是普通的文本 Hello World! ,如果在主地址后面添加 /index.html ,会显示一个标题 H3 的 Hello World! 。
静态内容的配置可以通过传入一个 StaticfileOptions 对象给 UseStaticFiles 方法来进行设置。最常用的配置属性是 FileProvider 和 RequestPath ,前者指定提供静态内容的物理地址,后者指定 Http 请求中的静态内容的路径。

管理客户端软件包

ASP.NET Core 项目中,客户端包用于将JavaScript、CSS、字体或其他资源添加到应用程序中。有效管理这些包可确保开发过程顺利进行。如何在 ASP.NET Core 应用程序中使用和管理客户端软件包? 我们现在有下面几个实践较好的选择。

使用 LibMan(库管理者)

微软提供了一个和 Visual Studio 整合的轻量级包管理工具 LibMan ,我们也可以在终端命令行使用它。一般说来,这个工具是首选:

  • 简单:快速方便,是为 ASP.NET Core 定制
  • 轻量:不需要额外的依赖和配置
  • 按需使用:随时安装和删除软件包

我们需要先用终端命令行来安装。

Listing 14-22 一般是全局安装这个工具

dotnet tool uninstall --global Microsoft.Web.LibraryManager.Cli
dotnet tool install --global Microsoft.Web.LibraryManager.Cli

我们没有指定版本,默认安装最新版。

我们在终端里,转到项目文件夹,执行下面的命令初始化 LibMan 工具。
Listing 14-23 初始化 LibMan

libman init -p cdnjs

-p 选项指定提供者是 cdnjs。这个命令会创建一个 Json 文件:

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": []
}

当我们安装或者移除包时,这个文件内容会自动被修改。最好不要手动修改它。如果客户端包有缺失,可以运行命令 libman restore ,就会按 Json 文件的设置自动恢复软件包。

安装客户端软件包

我们先安装一个 Bootstrap 包:

libman install bootstrap -d wwwroot/lib/bootstrap

-d 选项是指定软件包的目的文件夹。安装完成后,如果我们查看 libman.json ,将会看到在 libraries 里自动被添加了一个 bootstrap 库的入口。

要使用 bootstrap ,我们可以在静态文件 Index.html 里添加引用链接。
Listing14-23 在 wwwroot 文件夹里的 Index.html 文件添加 bootstrap

<!DOCTYPE html>

<head>
    <link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css" />
    <title>Index-wwwroot</title>
</head>

<body>
    <h3 class="bg-primary text-white text-center">Hello World!</h3>
</body>

</html>

修改后,这个静态文件将会在客户端显示一条蓝底居中白字。

使用 npm(Node Package Manager)

npm 是最流行的功能超强的 JavaScript 库管理工具。npm 使用超级广泛,被绝大部分软件环境支持;是整合 ASP.NET Core 和现代前端框架(React,Angular,Vue 等)的必备工具;高效管理包的依赖和版本。

在终端检查 node 版本

node -v

如果没有正确显示版本,请去 Nodejs官网 安装。

我们先移除 LibMan 安装的 bootstrap 。

libman uninstall bootstrap

我们先用命令行在 wwwroot 文件夹下新建一个 lib 文件夹。

mdkir wwwroot/lib

在终端,改变当前文件夹至 wwwwroot/lib 。运行 npm 安装命令:

npm i bootstrap

npm 在 lib 文件夹里自动新建 node_modules 文件夹和 package.json 以及 package-lock.json 文件。修改 Index.html 文件的 bootstrap 引用为:

    <link rel="stylesheet" href="/lib/node_modules/bootstrap/dist/css/bootstrap.min.css" />

使用 CDN

对于一些小型应用,直接使用 CDN 是合适的选择。

在一些复杂的应用系统结构里,甚至应该考虑使用 webpack 或者 gulp 等自动任务工具来管理应用程序的资源。

对于客户端软件包管理,要遵循以下原则:

  • 依赖包最小化:只安装必要的库
  • 版本控制:锁定库版本防止不必要的更新
  • 组合和最小化资源:达到减少 HTTP 请求和提升载入时间的目的
  • 监控表现:测试和监控客户端软件包的表现,纠错和提高效率

使用 Cookies

Cookie 是网络应用程序在用户浏览器中存储用户特定信息的一种方式;它 是客户端浏览器存储的小段数据,每次 HTTP 请求都会发送回服务器。ASP.NET Core 通过组件的 HttpRequest 和 HttpResponse 对象提供 Cookie 支持。
Cookie 主要用于保存用户信息(如用户偏好、身份验证令牌),以及在无状态 HTTP 通信中维护会话状态。

我们基本上使用两类 Cookie:

  • 会话 Cookie : 此类 Cookie 存储在客户端内存里,随着浏览器关闭会失效。
  • 持久 Cookie : 此类 Cookie 存储在客户端的磁盘上,在规定的有效期过后失效。

设置 Cookies

我们试着用 HttpContext 的 API 给应用添加一个 Cookie 。

Listing 14-24 修改 Program.cs 文件,添加 Cookie

...
app.MapGet("/", () => "Hello World!");

app.MapGet("/set-cookie", (HttpContext context) => {
    context.Response.Cookies.Append(
        "MyCookie",  // Cookie name
        "MyValue",  // Cookie value
        new CookieOptions
        {
            HttpOnly = true, // This cookie is not accessible via JavaScript
            Secure = true, // This cookie is only sent to the server over HTTPS
            SameSite = SameSiteMode.Strict, // This cookie is not sent on cross-site requests
            Expires = DateTimeOffset.Now.AddMinutes(10) // This cookie expires in 10 minutes
        }
    );
    return "Cookie is set";
});

app.Run();

我们在 Append 方法设置 Cookie 名字,值和配置选项。接下来要用 dotnet run -lp https 运行应用,因为我们设置了 Secure = true 的选项;在浏览器(我是用 Microsoft Edge)地址栏输入 https://localhost:5001/set-cookie ,网页会显示 Cookie is set 文本。
想要查看 Cookie 是否正确设置,我们可以在页面点击鼠标右键,选择检查,再从 tab 栏里选择”应用”(Application);然后点击左边的 Storage 下面的 Cookies 项目,就可以看到右边表格更新了,显示”MyCookie“条目:

浏览器 Cookie 检查

Figure 14-1

读取 Cookies

我们用 TryGetValue 方法读取 Cookie :
Listing 14-25 在 Program.cs 里添加读取 Cookie 的方法

...
app.MapGet("/set-cookie", (HttpContext context) => {
    context.Response.Cookies.Append(
        "MyCookie",  // Cookie name
        "MyValue",  // Cookie value
        new CookieOptions
        {
            HttpOnly = true, // This cookie is not accessible via JavaScript
            Secure = true, // This cookie is only sent to the server over HTTPS
            SameSite = SameSiteMode.Strict, // This cookie is not sent on cross-site requests
            Expires = DateTimeOffset.Now.AddMinutes(10) // This cookie expires in 10 minutes
        }
    );
    return "Cookie is set";
});

app.MapGet("/get-cookie", (HttpContext context) => {
    var cookieValue = context.Request.Cookies.TryGetValue("MyCookie", out var value)
        ? value
        : "not found";
    return $"Cookie value is {cookieValue}";
});

app.Run();

删除 Cookies

我们当然不能在后台服务里删除 Cookies ,实际上,Delete 方法是发送一个过期的 Cookie 版本给客户端,来触发客户端移除 Cookie 。

Listing 14-26 在 Program.cs 里添加清除 Cookie 方法

...
app.MapGet("/clear-cookie", (HttpContext context) => {
    context.Response.Cookies.Delete("MyCookie");
    return "Cookie is cleared";
});
...

Cookie 验证

Cookies 最广泛的应用是用于验证。在 ASP.NET Core 里,我们可以用 Cookie 验证中间件来处理验证令牌(token)。

我们给主入口里的 builder 添加验证服务。

Listing 14-27 *修改 Program.cs 添加 Cookie 验证服务和 POST 方法。

using System.Security.Claims;
using ASPNETCoreFeatures.Endpoints;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.HttpLogging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(static options => {
    options.LoggingFields = HttpLoggingFields.RequestProperties
        | HttpLoggingFields.ResponsePropertiesAndHeaders
        | HttpLoggingFields.ResponseStatusCode;
});

builder.Services.AddAuthentication("CookieAuth")
    .AddCookie("CookieAuth", options =>
    {
        options.LoginPath = "/login"; // Redirect to /login if unauthorized
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Expire after 30 minutes 
        options.SlidingExpiration = true; // Reset expiration time after each request
    });

var app = builder.Build();

...

app.MapGet("/clear-cookie", (HttpContext context) => {
    context.Response.Cookies.Delete("MyCookie");
    return "Cookie is cleared";
});

app.MapPost("/login", async (HttpContext context) => {
    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, "Alice"),
        new Claim(ClaimTypes.Role, "Admin")
    };

    var claimsIdentity = new ClaimsIdentity(claims, "CookieAuth");
    var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

    await context.SignInAsync("CookieAuth", claimsPrincipal);

    return Results.Ok("Logged in");
});

app.Run();

在一个终端(powershell)运行应用,再打开新的终端(powershell)里执行 Curl 命令:

 curl -X POST http://localhost:5000/login

新终端会显示响应 “Logged in”;而在运行应用的终端会显示类似日志:

info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[1]
      Request:
      Protocol: HTTP/1.1
      Method: POST
      Scheme: http
      PathBase:
      Path: /login
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
      Response:
      StatusCode: 200
      Content-Type: application/json; charset=utf-8
      Date: Sat, 18 Jan 2025 21:06:44 GMT
      Server: Kestrel
      Cache-Control: [Redacted]
      Expires: Thu, 01 Jan 1970 00:00:00 GMT
      Pragma: [Redacted]
      Set-Cookie: [Redacted]
      Transfer-Encoding: chunked

由于是 POST 请求,用 Curl 或者 Postman 等工具来测试比较方便,用 Powershell 的 Invoke-WebRequest 也行,只是不太流行而已。

Cookie 政策中间件

ASP.NET Core 包含 Cookie 政策设置的一个中间件:

Listing 14-28 在 Program.cs 中增加 Cookie 中间件

...

var app = builder.Build();

app.UseCookiePolicy(new CookiePolicyOptions
{
    MinimumSameSitePolicy = SameSiteMode.Strict,
    HttpOnly = HttpOnlyPolicy.Always,
    Secure = CookieSecurePolicy.Always
});

app.UseHttpLogging();

app.UseStaticFiles();

app.MapGet("coffee/{type?}", Coffee.GetCoffee);

app.MapGet("/", () => "Hello World!");

app.MapGet("/set-cookie", (HttpContext context) => {
    context.Response.Cookies.Append(
        "MyCookie",  // Cookie name
        "MyValue",  // Cookie value
        new CookieOptions
        {
            // HttpOnly = true, // This cookie is not accessible via JavaScript
            // Secure = true, // This cookie is only sent to the server over HTTPS
            // SameSite = SameSiteMode.Strict, // This cookie is not sent on cross-site requests
            Expires = DateTimeOffset.Now.AddMinutes(10) // This cookie expires in 10 minutes
        }
    );
    return "Cookie is set";
});

...

设置了政策以后,就可以把 /set-cookie 方法里的三行注释掉。

使用会话 Sessions

ASP.NET Core 中使用会话是在用户在服务器端存储数据的常用方法。这些数据可在多个 HTTP 请求中使用,对于维护用户特定信息(如登录凭据、购物车数据或临时设置)特别有用。

会话允许我们在服务器端存储和检索特定于用户的数据,同时将其与唯一标识符(会话 ID)相关联。会话 ID 通常存储在 cookie 中,或在请求 URL 中传递。

会话的优点

  • 易于用于短期数据存储。
  • 客户端不能直接存取数据,与客户端存储(如 cookie 或 localStorage)相比,安全性更高。
  • 数据可在用户会话的多个请求中使用。

使用会话的场合

  • 存储临时用户特定数据(如用户偏好、临时设置或购物车)。
  • 用于不需要在用户会话之后持续存在的数据。
  • 不想在客户端和服务器之间来回发送敏感数据时。

配置会话服务和中间件

我们先在主入口 Program.cs 里添加会话服务和中间件:

Listing 14-29 修改 Program.cs ,添加会话

...

builder.Services.AddAuthentication("CookieAuth")
    .AddCookie("CookieAuth", options =>
    {
        options.LoginPath = "/login"; // Redirect to /login if unauthorized
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Expire after 30 minutes 
        options.SlidingExpiration = true; // Reset expiration time after each request
    });

builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

app.UseSession();

app.UseCookiePolicy(new CookiePolicyOptions
{
    MinimumSameSitePolicy = SameSiteMode.Strict,
    HttpOnly = HttpOnlyPolicy.Always,
    Secure = CookieSecurePolicy.Always
});

...

当我们使用会话时,我们要决定如何存储会话数据。ASP.NET Core 提供了三种选项来存储会话数据,要根据实际情况选择。

名字描述
AddDistributedMemoryCache用于设置内存缓存。名字是分布式,但这个缓存只负责为创建它的应用实例存储数据。
AddDistributedSqlServerCache该方法设置缓存,用于在 SQL Server 中存储数据,应用必须添加 Microsoft.Extensions.Caching.SqlServer 软件包。
AddStackExchangeRedisCache此方法用于设置 Redis 缓存,在安装 Microsoft.Caching.Redis 软件包后可用。

前面表格总结过,AddDistributedMemoryCache 方法创建的缓存服务不是分布式的,而是为 ASP.NET Core 运行时的单个实例存储会话数据。如果我们通过部署多个运行时实例来扩展应用程序,则应使用其他缓存,如 SQL Server 缓存。

会话配置设置里的 Cookie.IsEssential = ture 属性是指定应用必须强制需要 Cookie 才能运行。它也有类似 Cookie 配置的 HttpOnly 和 SecurityPolicy 的属性。

使用会话的数据

会话数据以 键-值 的方式保持,
ASP.NET Core 会话中间件提供了许多属性和扩展方法,方便我们使用会话的数据:

  • Clear(): 该方法删除会话中的所有数据。
  • CommitAsync(): 此异步方法会将已更改的会话数据提交到缓存中。
  • GetString(key): 该方法使用指定的键检索字符串值。
  • GetInt32(key): 该方法使用指定的键检索整数值。
  • Id: 该属性返回会话的唯一标识符。
  • IsAvailable: 此属性在会话数据已加载时返回 true。
  • Keys: 该属性枚举会话数据项的键。
  • Remove(key): 该方法删除与指定键相关联的值。
  • SetString(key, val): 该方法使用指定的键存储字符串。
  • SetInt32(key, val): 该方法使用指定的键存储一个整数。

我们试着使用其中的方法来访问会话。在这之前先准备一个演示数据类:

Listing 14-30 在项目文件夹下新建文件夹 Session ,在其中新建文件 Persion.cs

namespace ASPNETCoreFeatures.Session
{
    public class Person
    {
        public string? Name { get; set; }
        public int Age { get; set; }
        public string? Email { get; set; }
    }
}

在主入口里添加会话服务:

Listing 14-31 在 Program.cs 里添加会话服务和端点方法

...

builder.Services.AddAuthentication("CookieAuth")
    .AddCookie("CookieAuth", options =>
    {
        options.LoginPath = "/login"; // Redirect to /login if unauthorized
        options.ExpireTimeSpan = TimeSpan.FromMinutes(30); // Expire after 30 minutes 
        options.SlidingExpiration = true; // Reset expiration time after each request
    });

builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.IsEssential = true; // Make the session cookie essential
});

var app = builder.Build();

app.UseSession();

app.UseCookiePolicy(new CookiePolicyOptions
{
    MinimumSameSitePolicy = SameSiteMode.Strict,
    HttpOnly = HttpOnlyPolicy.Always,
    Secure = CookieSecurePolicy.Always
});

...

app.MapGet("/set-session", (HttpContext context) => {
    context.Session.SetString("Name", "Alice");
    context.Session.SetInt32("Age", 25);
    return "Session is set";
});

app.MapGet("/get-session", (HttpContext context) => {
    var name = context.Session.GetString("Name") ?? "not found";
    var age = context.Session.GetInt32("Age") ?? 0;
    return $"Name: {name}, Age: {age}";
});

app.MapGet("/clear-session", (HttpContext context) => {
    context.Session.Clear();
    return "Session is cleared";
});

app.MapGet("/set-object-session", (HttpContext context) => {
    var person = new Person { Name = "Alice", Email = "alice@example.com" };
    context.Session.SetString("Person", JsonSerializer.Serialize(person));
    return "Object session is set";
});

app.MapGet("/get-object-session", (HttpContext context) => {
    var person = context.Session.GetString("Person");
    var personObj = person != null
        ? JsonSerializer.Deserialize<Person>(person)
        : null;
    return personObj != null
        ? $"Name: {personObj.Name}, Email: {personObj.Email}"
        : "Person object not found";
});

app.Run();

请记得要在文件开头添加引用:

using System.Text.Json;
using ASPNETCoreFeatures.Session;

如果运行应用后,在前面的会话端点没有成功显示预定信息,除了代码错误的原因,很可能是浏览器 Cookie 功能没有开启。

异常和错误处理

在开发过程中,我们希望发生异常和错误时,系统能尽可能多的有效信息,方便纠错和调试。然而,在生产版本里,出于安全性考虑,我们不希望异常和错误信息泄露给其他人;通常,我们会显示友好的回应给用户,以免造成用户担忧。

Listing 14-31 更改 Properties 文件夹里的 launchSettings.json 里的环境配置

...

    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Production"
      }
    },

...

Listing 14-32 在 Program.cs 里添加返回静态错误页面的异常处理服务

...

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error.html");
}

app.UseSession();

...

Listing 14-33 在 wwwroot 文件夹下新建 error.html 静态文件

<!DOCTYPE html>

<head>
    <link rel="stylesheet" href="/lib/bootstrap/css/bootstrap.min.css" />
    <title>Error!</title>
</head>

<body class="text-center">
    <h3 class="p-3">Something wrong occured ...</h3>
    <p class="text-danger">Please contact admin!</p>
</body>

</html>

我们现在给主入口添加一个人为的抛出异常。

Listing 14-34 在 Program.cs 文件添加抛出异常

...

app.Run(context =>
{
    throw new Exception("Some error occurred");
});

app.Run();

dotnet run -lp https 运行应用,在浏览器里输入 https://localhost:5001 ,就会显示错误静态页面。
我们这里只是对异常做了最初步的处理,在实际应用中,要加上更多的信息和链接等,帮助用户恢复到相对正常的应用路径或者初始状态。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注