.NET Core 2.0 开始支持标准 2.0 是 .NET 的里程碑,是现代开发之路的起点。开发者终于有了可以跨平台的能力,.NET 可以在 Windows,Linux 和 macOS 三个平台运行。.NET 5 移除了对 .NET Standard 的依赖,所有的之后的版本都是向前兼容的。也就是说,一个目标是 .NET 5 的类库可以被所有之后的版本使用。从 .NET 7 开始完全支持移动应用的 .NET MAUI 开发,但 7 是一个非长期支持的版本;当长期支持版本 .NET 8 出现后,这个功能超强的开发工具愈发成熟和高效,再加上 Blazor WASM 的不断进步,C# 和 ASP.NET Core 成为备受开发者青睐的现代开发环境。

检查 .NET SDK 环境

dotnet sdk check 

稍等片刻,终端显示我的机器上内容如下:

.NET SDKs:
Version      Status
----------------------------------------
6.0.424      .NET 6.0 is out of support.
8.0.303      Patch 8.0.307 is available.
8.0.400      Patch 8.0.404 is available.

Try out the newest .NET SDK features with .NET 9.0.101.

.NET Runtimes:
Name                              Version      Status
--------------------------------------------------------------------------
Microsoft.AspNetCore.App          6.0.32       .NET 6.0 is out of support.
Microsoft.NETCore.App             6.0.32       .NET 6.0 is out of support.
Microsoft.WindowsDesktop.App      6.0.32       .NET 6.0 is out of support.
Microsoft.AspNetCore.App          8.0.7        Patch 8.0.11 is available.
Microsoft.NETCore.App             8.0.7        Patch 8.0.11 is available.
Microsoft.WindowsDesktop.App      8.0.7        Patch 8.0.11 is available.
Microsoft.AspNetCore.App          8.0.8        Patch 8.0.11 is available.
Microsoft.NETCore.App             8.0.8        Patch 8.0.11 is available.
Microsoft.WindowsDesktop.App      8.0.8        Patch 8.0.11 is available.

可以看到 6.0 已经不被支持了,而我有 2 个 8.0 版本的 SDK , 因此,我都会在 global.json 中指定 APP 使用的 SDK 版本。

.NET 组件
.NET 最主要由 3 个组件:

  • 语言编译器(Language Compilers): 这些编译器把 C#, F# 和 Visual Basic 语言编写的代码转换为中间语言 IL (intermediate language)。在 C# 6 以后,微软使用一个开源的 Roslyn 编译器来完成任务。
  • 通用语言运行时 CLR (Common Language Runtime):这个运行时将载入程序集,并把其中的中间语言编译为本地代码指令,让 CPU 使用,在可以管理线程和内存的环境里执行代码。
  • 基础类库 BCL(Base Class Libraries):这些基础类库是在内置的程序集里,用 NuGet 打包分发,执行构建应用时用到的常见任务。

程序集是部署代码的机制,使用其中的类型和方法时可能需要加以引用
命名空间就像是类型或方法的地址,可以给它们唯一的名字
如果一个程序集编译为类库时,它不能独立执行,但要编译为动态链接库 DLL ,可以被其他程序集引用和使用。

有了这些基础知识以后,让我们开始稍微深入 ASP.NET Core 8;如果有什么预备知识以上没有涉及,我们会在主题里再介绍。

虽然现在( 2024 年底)微软都已经发布了 net9.0 了,但是实际开发中,大概率会用到各种的NuGet 包,每个包都有蓝底白字的目标框架说明,什么 netstandard2.0,net6.0,net8.0 等等,那该如何确定是否兼容正在开发的应用呢?请参照下面表格,基本就不出意外了:

类库的目标框架能被具有如下目标框架的项目使用
netstandard2.0.NET Framework 4.6.1 及后续,.NET Core 2 及后续, .NET 5 及后续,Mono 5.4 及后续,Xamarin.Android 8 及后续,Xamarin.iOS 10.14 及后续
netstandard2.1.NET Core 3 及后续,.NET 5 及后续,Mono 6.4 及后续, Xamarin.Android 10 及后续,Xamarin.iOS 12.16 及后续
net6.0.NET 6 及后续
net7.0.NET 7 及后续
net8.0.NET 8 及后续

因此我们在开发中,完全可以混装勾兑各种类库,但是有风险,需谨慎。

深入 ASP.NET Core 8 平台

要理解 ASP.NET Core ,我们要先了解它的关键特性:请求管道(request pipeline), 中间件(middleware)和服务(Services),以及这三个特性是如何协同工作的。

请求管道和中间件

概括起来说,ASP.NET Core 平台是用来接收 HTTP 请求并发送响应的;在接收和发送中间阶段,用中间件组件处理相关程序逻辑。请求到响应就像一条管道,管道中存在若干的中间件。
在一般情况下,每个中间件接收并处理上一个中间件处理后的请求和响应对象,再击鼓传花把它们传给下一个中间件,直到响应对象经过按程序指定的所有中间件(我们可以旁路中间件),最后传给主体程序逻辑生成响应,然后,AspNetCore 平台再在管道中按反方向让响应也经过所需中间件,如果需要,可以由中间件更改响应,最后才发送响应给客户端。
如果最后没有得到任何响应,AspNetCore 平台将回复 HTTP 404 状态码。
中间件能够处理请求,决定是否响应,或者把请求传给管道中的下一个中间件。

ASPNETCORE 中间件和管道

Figure 12-1

图 12-1 是典型的MVC的管道和中间件处理流程,我们可以看到不只请求需要被所需的中间件过滤一遍,响应也是要被同样过一遍的,只是方向相反。

服务

服务对象用来给应用添加应用特性。任何类都可以作为服务,Asp.Net Core 用依赖注入来管理服务,因此,中间件也能够使用服务。

ASPNETCORE服务

Figure 12-2

Asp.NET Core 项目

在用命令行或者 Visual Studio 的图形环境建立新的 Asp.NET Core 项目后,我们在 IDE 中查看项目文件夹结构时,Visual Studio 和 Visual Studio Code 两者呈现的方式有点不同,前者会隐藏 bin ,obj 等文件夹,并且把一些项目配置文件整合成按钮方便用户管理。而 VS Code 本质上是一个文本编辑器,它的左边项目文件夹里会显示所有文件,我觉得对理解项目文件结构更有帮助。
基础 Web 项目文件夹结构:

文件(夹)描述
appsettings.json项目的基本配置文件
appsettings.Development.json项目开发环境里的基本配置文件
bin这个文件夹里存放编译后的应用的文件
global.json经常用于指定 SDK 的版本
Properties/launchSettings.json项目启动配置文件
obj这个文件夹存放编译器中间语言输出的结果
项目名称.csproj文件描述 .NET Core 的各种工具,依赖的包和编译构建应用的指引
项目名称.sln便于管理项目的文本文件,类似项目目录
Program.cs应用的主入口,配置整个应用平台后运行应用

现在我们可以开始准备这一章的实例代码了。

dotnet new globaljson --sdk-version 8.0.400 --output ASPNETCoreInsight
dotnet new web --output ASPNETCoreInsight --framework net8.0
dotnet new sln --output ASPNETCoreInsight
dotnet sln ASPNETCoreInsight add ASPNETCoreInsight

修改 Properties 文件夹里的 launchSettings.json 文件,把 http 端口改为 5000,https 端口改为 5001。
在 Windows Terminal 或者 PowerShell 中输入运行命令:

dotnet run

或者

dotnet run -lp https

第一个命令是以 http 运行,第二个是以 https 运行。在浏览器输入相应地址,会得到 Hello World! 的显示页面。

入口点 Program.cs

Program.cs 文件的第一行是

var builder = WebApplication.CreateBuilder(args);

这个方法负责应用的基本特性设置,另外,它还要设置一个 HTTP 服务器 Kestrel ,用来接收 HTTP 请求。当然,这些设置都已经整合成一个动词加宾语 CreateBuilder
这个方法返回一个 WebApplicationBuilder 对象,我们要用它来添加附加服务。现在暂时没有附加服务,我们直接用这个对象的 Build 方法来完成初始设置。

var app = builder.Build();

Build 后的结果是 WebApplication 对象,我们给它添加一个中间件—— 一个扩展方法 MapGet() 。MapGet 是一个 IEndpointRouteBuilder 接口的扩展方法,在 WebApplication 类里实现这个方法。这个方法指定处理 HTTP 请求的 URL ,在这里指定 / URL ,并返回一个字符串。

项目文件 .csproj

项目文件包含 .NET Core 构建应用的信息和其他的依赖。项目文件按 XML 格式存储内容,我们通过 dotnet 命令行添加的任何依赖都会在项目文件里添加条目;反过来,我们可以手动添加条目,只要格式正确,重新 restore 和 build 后,也会自动下载和安装依赖软件包。

添加中间件

我们可以使用内建的中间件,也可以手动添加自己的中间件。我们用方法 Use() 添加中间件。
Listing 12-1 修改 Program.cs 文件,添加中间件

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

app.Use(async (context, next) =>
{
    if (context.Request.Method == HttpMethods.Get && context.Request.Query["name"].Count > 0)
    {
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync($"Welcome {context.Request.Query["name"]}! n");
    }
    await next();
});


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

app.Run();

我们添加一个中间件:如果用户的查询字符串里有 name 键值并且内容不为空,就在响应里添加一段欢迎的字符串。我们用 Lambda 添加一个代理。
运行应用后,在浏览器地址栏的整个地址 http://localhost:5000/后面添加查询 ?name=John Smith ,发送后,页面将在 Hello World! 上面显示一串字符:Welcome John Smith! 。表明我们确实把中间件添加到响应里面了。
Query是 Http 请求的成员之一。
常用的 HttpRequest 的成员:

成员名描述
Body 属性返回请求体的串流
ContentType 属性返回请求头里的内容类型
Form 属性返回作为表单 Form 的请求体
Headers 属性返回请求头
Method 属性返回 HTTP 请求方法
Query 属性返回请求 URL 里的查询部分,键-值格式

既然介绍了请求,再介绍下响应 Response :

成员名描述
ContentType 属性设置响应头中的 Content-Type 值
StatusCode 属性设置响应里的状态码
Redirect 方法这个方法发送一个跳转到指定 url 的响应

我们在前面的中间件里定义了请求方法是 GET , 查询属性的键是 “name” ;接着,定义响应的内容类型是 “text/plain” ,再把字符串写入响应里。
Use 方法的第二个参数是框架的约定,指请求管道中的下一个组件的代理, 它是异步代理,需要用 await。

关于中间件,更常见的做法是定义中间件类。
我们在项目文件夹里新建一个 Middlewares 文件夹,再添加中间件类。
Listing 12-2 在 Middlewares 文件夹里新建 QueryMiddleware.cs 文件

namespace ASPNETCoreInsight.Middlewares
{
    public class QueryMiddleware(RequestDelegate next)
    {
        private readonly RequestDelegate _next = next;

        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Method == HttpMethods.Get && context.Request.Query["name"].Count > 0)
            {
                context.Response.ContentType = "text/plain";
                await context.Response.WriteAsync($"Welcome {context.Request.Query["name"]}! n");
            }
            await _next(context);
        }
    }
}

中间件类接受一个请求代理作为构建函数的参数,用于把请求传给下一个组件。Invoke 方法处理中间件的逻辑,这里的请求代理需要知道请求上下文,因此需要加入 HttpContext 类的参数。前面我们在主入口 Program.cs 中使用 Use 方法是不用的,因为方法就在这个主入口类中,Http 上下文就是默认就是同一个。

接着,我们要用 UseMiddleware 方法在 Webapplication 对象里加入中间件类。
Listing 12-3 修改 Program.cs 文件,取消 Use 方法,加入 QueryMiddleware 中间件

using ASPNETCoreInsight.Middlewares;

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

app.UseMiddleware<QueryMiddleware>();

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

app.Run();

这样,主入口 Program.cs 就显得优雅多了。

短路请求管道

有时会有某些场合,不想让请求通过某个中间件,我们可以让请求管道短路,直接返回响应给客户端。
Listing 12-4 修改 Program.cs ,添加一个短路中间件

using ASPNETCoreInsight.Middlewares;

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

app.Use(async (context, next) => {
    if (context.Request.Path == "/ignoresth")
    {
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("Welcome! n");
    } 
    else 
    {
        await next();
    }
});

app.UseMiddleware<QueryMiddleware>();

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

app.Run();

当我们再次运行应用时,输入 http://localhost:5000 ,浏览器显示 Hello World!,如果在前面的地址后面加查询 ?name=John Smith ,那么浏览器将显示:

Welcome John Smith!
Hello World!

但是如果在地址栏输入 http://localhost/ignoresth/?name=John Smith ,你们认为浏览器会输出什么内容?
如果你推导出内容只是 Welcome John Smith! ,那么恭喜你,你已经理解了短路请求管道的作用。确实,浏览器显示如下图:

管道示例

Figure 12-3

因为 QueryMiddleware 是通过依赖注入的,所以会通过这个中间件,而后面的 MapGet 方法在被执行以前,应用已经返回响应到客户端了,跳过了执行这个方法。

管道分支

框架提供一个 Map 方法,可以用来创建管道分支,允许我们根据给定的路径执行不同的管道和其中的中间件。

Listing 12-5 修改 Program.cs 添加管道分支

using ASPNETCoreInsight.Middlewares;

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

app.Use(async (context, next) => {
    if (context.Request.Path == "/ignoresth")
    {
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("Welcome! n");
    } 
    else 
    {
        await next();
    }
});

// Map 方法添加管道分支
app.Map("/branch", branchApp => {
    branchApp.UseMiddleware<QueryMiddleware>();
    branchApp.Run(async (HttpContext context) => await context.Response.WriteAsync( "Welcome To Branch App! n"));
});

app.UseMiddleware<QueryMiddleware>();

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

app.Run();

当我们输入 branch 路径时,应用把请求送入分支管道,分支管道把响应返回客户端。注意,当 HTTPContext 传入分支管道时,它变成只读,不能再对他进行修改。
在 Lambda 里的后面一个语句是终结中间件,不再把请求传给其他组件,到此为止就返回响应。
运行应用,我们将看到分支管道返回响应对象,不再使用原来的请求管道。

管道分支示例

Figure 12-4

配置中间件

在很多内置中间件里会用一种常见的模式来配置中间件,这种模式称为选项模式(Options pattern)。使用这个模式请,我们先定义一个选项类,再定义一个新的中间件并在其中使用选项类。

Listing 12-5 在 Middlewares 文件夹里新建 SeeYouAgain.cs 文件

using Microsoft.Extensions.Options;

namespace ASPNETCoreInsight.Middlewares
{
    public class SeeYouAgain
    {
        private readonly RequestDelegate _next;
        private readonly SeeYouAgainGreet _greet;
        public SeeYouAgain(RequestDelegate next, IOptions<SeeYouAgainGreet> options)
        {
            _next = next;
            _greet = options.Value;
        }

        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Path == "/seeyouagain")
            {
                await context.Response.WriteAsync(_greet.Greet(context.Request.Query["name"]));
            }
            else
            {
                await _next(context);
            }
        }
    }
    public class SeeYouAgainGreet
    {
        public string Greet(string? name)
        {
            name ??= "Stranger";
            return $"Nice to see you again, {name}!";
        }
    }
}

接着我们在 Program.cs 里加入这个中间件。

Listing 12-6 修改 Program.cs 文件,加入 SeeYouAgain 中间件

...
app.Map("/branch", branchApp => {
    branchApp.UseMiddleware<QueryMiddleware>();
    branchApp.Run(async (HttpContext context) => await context.Response.WriteAsync( "Welcome To Branch App! n"));
});

app.UseMiddleware<QueryMiddleware>();
app.UseMiddleware<SeeYouAgain>();

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

app.Run();

运行应用,在浏览器地址栏输入http://localhost:5000/seeyouagain?name=John%20Smith ,页面将显示下图:

配置中间件

Figure 12-5

发表回复

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