ASP.NET Core知识点详解,小白能懂

1. Main 方法 (The Main Method)

大白话解释

Main 方法是所有 C# 程序的入口点,就像房子的大门一样。程序一启动,第一个执行的就是 Main 方法。

为什么需要它

ASP.NET Core 应用本质上是一个控制台应用,只不过它启动后会变成一个 Web 服务器。Main 方法就是用来完成这个 “变身” 过程的:它会配置并启动 Web 服务器来监听网络请求。

代码示例 (Program.cs)

csharp

// 这是 .NET 6 及以后版本的简化写法
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 定义一个简单的路由:当用户访问网站根目录 (/) 时,返回 "Hello, World!"
app.MapGet("/", () => "Hello, World!");

// 启动Web应用程序,开始监听端口
app.Run();
语法详解
  1. var builder = WebApplication.CreateBuilder(args);
    • WebApplication.CreateBuilder() 是一个 “工厂方法”,它会帮你创建一个 WebApplicationBuilder 对象。
    • 这个 builder 对象就像一个 “包工头”,负责帮你配置所有东西,比如服务(数据库连接、日志等)、中间件、配置文件等。
    • args 是从命令行传递给程序的参数。
  2. var app = builder.Build();
    • 让 “包工头” (builder) 根据你的配置,把房子(app,也就是 WebApplication 实例)真正盖好。这个 app 对象代表了你的整个 Web 应用。
  3. app.MapGet("/", () => "Hello, World!");
    • 这是在定义一个路由
    • "/" 代表网站的根路径(比如 http://localhost:5000/)。
    • () => "Hello, World!" 是一个 lambda 表达式,可以理解为一个简单的函数。当有人访问根路径时,就执行这个函数,并把返回的字符串 "Hello, World!" 发送给浏览器。
  4. app.Run();
    • 这是启动 Web 服务器的命令。程序会在这里 “卡住”,持续监听网络请求,直到你手动关闭它。
总结

Main 方法是ASP.NET Core 应用的起点,它的核心任务就是构建和启动 Web 服务器


2. 进程内托管 (In-Process Hosting)

大白话解释

进程内托管就是ASP.NET Core 应用直接 “搬进” IIS 进程里面去运行。IIS 就像一个大工厂,你的应用程序不是在工厂外自己开个小作坊,而是直接成为工厂里的一条生产线。

为什么需要它
  • 性能更高:因为应用和 IIS 在同一个进程内,通信不需要跨进程,速度更快,开销更小。
  • 部署简单:对于 Windows 服务器,这是非常自然的部署方式。
工作原理
  • IIS 接收到一个网络请求。
  • 请求直接传递给在同一个进程内运行的ASP.NET Core 应用。
  • 应用处理完请求后,直接通过 IIS 把响应发回给客户端。
总结

进程内托管是将 Web 应用和 Web 服务器(IIS)“融为一体”,以获得最佳性能。


3. 进程外托管 (OutOfProcess Hosting)

大白话解释

进程外托管就是ASP.NET Core 应用和 IIS 分开,在各自独立的进程里运行。IIS 就像一个 “门卫”,它负责接收所有外来的请求,然后把请求 “转发” 给在外面独立运行的ASP.NET Core 应用。

为什么需要它
  • 跨平台:应用本身是一个独立的控制台程序,可以在 Windows、Linux、macOS 上运行,不受 IIS 限制。
  • 稳定性:如果应用程序崩溃了,它不会影响到 IIS 本身,IIS 可以尝试重新启动它。
  • 灵活性:可以使用ASP.NET Core 自带的、性能极高的 Kestrel 服务器作为真正的 Web 服务器。
工作原理
  1. IIS 接收到请求。
  2. IIS 将请求通过一个叫做 AspNetCoreModule (ANCM) 的模块,转发给独立运行的ASP.NET Core 应用。
  3. 应用(由 Kestrel 服务器托管)处理请求。
  4. 应用将响应发回给 IIS,再由 IIS 发送给客户端。
总结

进程外托管是将 Web 应用和 Web 服务器(IIS)“分离”,以获得更好的跨平台性和稳定性。在 .NET Core 2.2 之后,进程内托管成为在 Windows 上部署到 IIS 的默认选项。


4. launchsettings.json 文件

大白话解释

launchsettings.json 是一个项目启动配置文件。它告诉 Visual Studio(或其他开发工具)在你按 F5 运行项目时,应该用什么方式来启动它。

为什么需要它

它让你可以轻松地在不同的启动环境之间切换,比如:

  • 用 IIS Express 启动,方便在 Windows 上调试。
  • 用项目自托管(Kestrel)启动,方便模拟生产环境或在 macOS/Linux 上运行。
  • 为不同的启动方式设置不同的端口号、环境变量等。
代码示例 (Properties/launchsettings.json)

json

{
  "profiles": {
    "MyWebApp": { // 一个启动配置,名字叫 "MyWebApp"
      "commandName": "Project", // 启动方式:直接运行项目本身(自托管)
      "dotnetRunMessages": true,
      "launchBrowser": true, // 启动后自动打开浏览器
      "applicationUrl": "https://localhost:5001;http://localhost:5000", // 应用监听的URL和端口
      "environmentVariables": { // 设置环境变量
        "ASPNETCORE_ENVIRONMENT": "Development" // 环境为“开发环境”
      }
    },
    "IIS Express": { // 另一个启动配置,名字叫 "IIS Express"
      "commandName": "IISExpress", // 启动方式:通过IIS Express
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
语法详解
  • "profiles":包含了所有可用的启动配置。你可以在 Visual Studio 的启动项目下拉菜单中看到这些名字。
  • "commandName": "Project":指示使用 dotnet run 命令来启动应用,由 Kestrel 服务器托管。
  • "commandName": "IISExpress":指示使用 IIS Express 来启动和托管应用。
  • "launchBrowser": true:运行后自动用默认浏览器打开 applicationUrl
  • "applicationUrl":定义了应用监听的地址。通常会包含一个 https 和一个 http 地址。
  • "environmentVariables":在启动时设置的临时环境变量,非常有用。
总结

launchsettings.json 是你的开发启动 “控制面板”,让你可以轻松配置和切换不同的运行环境。


5. appsettings.json 文件

大白话解释

appsettings.json 是用来存放应用程序配置信息的文件,比如数据库连接字符串、API 密钥、日志级别等。

为什么需要它
  • 解耦配置:把配置(如连接字符串)和代码分离,这样修改配置时就不需要修改代码、重新编译和部署了。
  • 环境特定配置:你可以为开发环境、测试环境、生产环境分别创建不同的配置文件(如 appsettings.Development.json)。
代码示例 (appsettings.json)

json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=my_server;Database=my_db;User Id=my_user;Password=my_password;"
  },
  "AppSettings": {
    "SiteName": "我的第一个网站",
    "MaxUploadSize": 10485760 // 10MB
  }
}
语法详解
  • 这是一个标准的 JSON 文件,由键值对组成。
  • "Logging":用于配置日志系统的行为。
  • "AllowedHosts": "*":允许任何主机头访问,通常保持默认即可。
  • "ConnectionStrings":专门用来存放数据库连接字符串的约定俗成的节点。
  • "AppSettings":你可以自定义任何节点来存放你的应用特有配置。
总结

appsettings.json 是你的应用的 **“大脑记忆”**,用来存放那些可能会随着环境或需求变化的配置信息。


6. 中间件 (Middleware)

大白话解释

中间件是ASP.NET Core 处理 HTTP 请求的管道中的一个个 “关卡” 或 “过滤器”。一个请求从进入管道到返回响应,会依次经过多个中间件,每个中间件都可以:

  1. 处理请求:比如记录日志、验证身份。
  2. 将请求传递给下一个中间件
  3. 处理响应:比如添加 HTTP 头、压缩内容。
为什么需要它

它提供了一种非常灵活和模块化的方式来构建请求处理流程。你可以像搭积木一样,按需组合不同的中间件来实现功能,比如静态文件服务、身份验证、路由等。

代码示例 (Program.cs)

csharp

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

// 中间件 1: 日志记录
app.Use(async (context, next) =>
{
    Console.WriteLine($"请求来了: {context.Request.Path}");
    // 调用 next() 将请求传递给下一个中间件
    await next();
    Console.WriteLine($"响应走了: {context.Response.StatusCode}");
});

// 中间件 2: 静态文件
// 如果请求的是一个静态文件(如 CSS, JS, 图片),这个中间件会处理并返回,不会再往下传递
app.UseStaticFiles(); 

// 中间件 3: 路由和终结点
app.MapGet("/", () => "Hello from the endpoint!");

app.Run();
语法详解
  1. app.Use(...):用来将一个中间件添加到管道中。
  2. async (context, next) => { ... }:这是一个异步的中间件委托。
    • context (HttpContext):包含了当前请求和响应的所有信息,比如请求路径、参数、响应状态码等。
    • next (RequestDelegate):是一个代表 “下一个中间件” 的函数。调用 await next() 才会让请求继续往下走。
  3. app.UseStaticFiles():这是一个内置的中间件,用于提供对 wwwroot 文件夹中静态文件的访问。
  4. app.MapGet(...):也可以看作是一种特殊的终端中间件,它匹配路由并处理请求,通常不再调用 next()
总结

中间件是ASP.NET Core 的核心,请求处理管道就是由一系列中间件串联而成的。


7. 请求处理通道 (Request Processing Pipeline)

大白话解释

请求处理通道就是由一系列中间件组成的处理流程。你可以把它想象成一个流水线:一个 HTTP 请求从管道的一端进入,经过一个接一个的中间件(工人)处理,最终从管道的另一端出来,变成一个 HTTP 响应。

为什么需要它

它提供了一个统一、可扩展的架构来处理所有的网络请求。无论请求是获取一个网页、一张图片还是提交一个表单,都遵循这个标准流程,使得代码结构清晰,易于维护和扩展。

工作流程
  1. 请求进入:客户端(浏览器)发送一个 HTTP 请求到服务器。
  2. 依次通过中间件
    • 日志中间件:记录 “有一个请求来了”。
    • 身份验证中间件:检查用户是否已登录,如果没有,可能直接返回一个 “401 Unauthorized” 响应,中断管道。如果已登录,就把用户信息附加到 context 中,然后传递给下一个中间件。
    • 路由中间件:根据请求的 URL,决定应该由哪个控制器的哪个方法来处理这个请求。
    • MVC 中间件:执行找到的控制器方法(Action),渲染视图,生成 HTML。
  3. 响应返回:响应信息沿着管道反向返回,每个中间件都有机会再对响应做最后的处理(比如压缩),最终返回给客户端。
总结

请求处理通道是ASP.NET Core 处理所有网络请求的骨架和蓝图


8. 静态文件 (Static Files)

大白话解释

静态文件就是那些内容不会动态改变的文件,比如 CSS 样式表、JavaScript 脚本、图片(.jpg, .png)、字体文件等。

为什么需要它

网页不仅仅是 HTML 代码,还需要 CSS 来美化样式,JS 来实现交互,图片来丰富内容。这些文件需要被 Web 服务器直接发送给浏览器,而不需要服务器端代码进行处理。

如何使用
  1. 添加中间件:在 Program.cs 中添加 app.UseStaticFiles();
  2. 放置文件:将所有静态文件放在项目根目录下的 wwwroot 文件夹中。
代码示例

假设你的项目结构如下:

plaintext

MyWebApp/
├── wwwroot/
│   ├── css/
│   │   └── site.css
│   ├── js/
│   │   └── site.js
│   └── images/
│       └── logo.png
└── Program.cs

在 Program.cs 中启用静态文件服务:

csharp

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

// 启用静态文件服务
app.UseStaticFiles(); 

app.MapGet("/", () => "访问静态文件: <a href='/images/logo.png'>Logo</a>");

app.Run();

现在,你可以通过以下 URL 访问这些文件:

  • http://localhost:5000/css/site.css
  • http://localhost:5000/js/site.js
  • http://localhost:5000/images/logo.png
总结

UseStaticFiles 中间件是 Web 服务器提供前端资源的 “窗口”,它专门负责把 wwwroot 文件夹里的东西直接送给浏览器。


9. 开发者异常界面 (Developer Exception Page)

大白话解释

开发者异常界面是一个在开发时出现错误时,显示详细错误信息的友好页面。它会告诉你哪个文件的哪一行代码出错了,以及完整的错误堆栈信息。

为什么需要它

在开发和调试阶段,我们需要尽可能详细的错误信息来快速定位和修复问题。这个页面就是为此而生的。但是,这个页面包含了太多敏感信息(如服务器路径、代码结构),绝对不能在生产环境中启用!

如何使用

通常它会和环境变量结合使用,只在开发环境下启用。

代码示例 (Program.cs)

csharp

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

// 判断当前是否是开发环境
if (app.Environment.IsDevelopment())
{
    // 如果是,就使用开发者异常页面
    app.UseDeveloperExceptionPage();
}
else
{
    // 如果不是(比如是生产环境),就使用一个通用的错误处理页面
    app.UseExceptionHandler("/Home/Error");
}

// 故意制造一个错误
app.MapGet("/error", () =>
{
    throw new Exception("这是一个故意抛出的异常!");
});

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

app.Run();
语法详解
  1. app.Environment.IsDevelopment():检查 ASPNETCORE_ENVIRONMENT 环境变量是否设置为 "Development"。这个变量通常在 launchsettings.json 中配置。
  2. app.UseDeveloperExceptionPage():启用开发者异常处理中间件。当管道中任何后续中间件抛出未处理的异常时,这个中间件会捕获它,并返回一个详细的 HTML 错误页面。
总结

开发者异常界面是开发人员的 **“调试神器”**,但在部署到生产环境前,一定要记得把它关掉。


10. 环境变量 (Environment Variables)

大白话解释

环境变量是存储在操作系统中,供应用程序读取的键值对。你可以把它想象成贴在服务器上的 “便利贴”,上面写着一些配置信息(比如 “我是生产环境”、“数据库密码是 xxx”)。

为什么需要它
  • 安全:避免将敏感信息(如数据库密码、API 密钥)硬编码在代码或配置文件中,防止泄露。
  • 灵活性:同一个应用部署在不同环境(开发、测试、生产)时,可以通过设置不同的环境变量来改变其行为,而无需修改代码。
如何在ASP.NET Core 中使用
  1. 在 launchsettings.json 中设置(用于开发):这是最方便的方式。
  2. 在操作系统中设置(用于生产)
    • Windows (PowerShell)$env:ASPNETCORE_ENVIRONMENT="Production"
    • Linux/macOS (bash)export ASPNETCORE_ENVIRONMENT=Production
  3. 在代码中读取
代码示例 (Program.cs)

csharp

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

// 从配置中读取环境变量
// 配置系统会自动读取环境变量,并使其可以通过 IConfiguration 服务访问
var envName = app.Configuration["ASPNETCORE_ENVIRONMENT"];

app.MapGet("/", () => $"当前应用运行在 '{envName}' 环境中。");

app.Run();
语法详解
  • app.Configuration["ASPNETCORE_ENVIRONMENT"]IConfiguration 服务是ASP.NET Core 内置的,它可以统一读取来自 appsettings.json、环境变量、命令行参数等多种来源的配置。这里我们用它来读取名为 ASPNETCORE_ENVIRONMENT 的环境变量。
总结

环境变量是配置应用行为、管理敏感信息的最佳实践,是连接应用程序和其运行环境的桥梁。

11. 依赖注入 (Dependency Injection, DI)

大白话解释

依赖注入,简单来说,就是 **“谁需要什么,我就主动给你什么,你不用自己去创建”**。

想象一下你去一家高档餐厅点牛排。

  • 没有 DI 的情况:你告诉服务员你要牛排。服务员说:“好的,请你自己去后院养一头牛,宰杀、烹饪好再端上来。” 这显然很荒谬。
  • 有 DI 的情况:你告诉服务员你要牛排。服务员直接从厨房(一个 “容器”)里拿出已经烹饪好的牛排给你。你只需要 “消费” 牛排,而不需要关心它是怎么来的。

在程序中,“你” 就是一个类(比如 HomeController),“牛排” 就是你需要的另一个类或服务(比如 ILogger 或 IEmailService)。

为什么需要它
  1. 解耦 (Decoupling):类与类之间的关系变得松散。HomeController 只需要知道 IEmailService 这个接口,不需要知道它的具体实现是 SmtpEmailService 还是 MockEmailService
  2. 易于测试 (Ease of Testing):因为依赖是注入的,在写单元测试时,我们可以很容易地用一个 “假的” 服务(Mock)来代替 “真的” 服务,从而隔离被测试的代码。
  3. 集中管理 (Centralized Management):所有服务的创建和生命周期都由一个中央容器管理,方便统一配置和维护。
代码示例

假设我们有一个简单的问候服务。

1. 定义服务接口和实现

csharp

// 服务接口 (IService.cs)
public interface IGreetingService
{
    string GetGreeting();
}

// 服务实现 (GreetingService.cs)
public class GreetingService : IGreetingService
{
    public string GetGreeting()
    {
        return "Hello from DI!";
    }
}

2. 在 Program.cs 中注册服务这就像告诉餐厅的厨房:“如果有人要 IGreetingService,你就给他 GreetingService。”

csharp

var builder = WebApplication.CreateBuilder(args);

// ... 添加其他服务,如 AddControllersWithViews() ...
builder.Services.AddControllersWithViews();

// **注册我们的自定义服务**
// 当需要 IGreetingService 时,创建一个新的 GreetingService 实例。
builder.Services.AddTransient<IGreetingService, GreetingService>();

var app = builder.Build();
// ...

3. 在控制器中注入并使用服务

csharp

// HomeController.cs
public class HomeController : Controller
{
    private readonly IGreetingService _greetingService;

    // **通过构造函数注入**
    // ASP.NET Core 框架在创建 HomeController 实例时,
    // 会自动查找并注入一个 IGreetingService 的实例。
    public HomeController(IGreetingService greetingService)
    {
        _greetingService = greetingService;
    }

    public IActionResult Index()
    {
        // **直接使用注入的服务**
        var message = _greetingService.GetGreeting();
        ViewData["Message"] = message;
        return View();
    }
}
语法详解
  • builder.Services.AddTransient<IGreetingService, GreetingService>();
    • builder.Services:这是服务容器,所有需要被注入的服务都在这里注册。
    • AddTransient:这是一种服务生命周期(我们后面会详细讲),表示每次请求服务时,都创建一个新的实例。
    • <IGreetingService, GreetingService>:泛型参数,左边是服务类型(通常是接口),右边是具体实现类型
  • public HomeController(IGreetingService greetingService):这是构造函数注入。当框架需要一个 HomeController 时,它会检查其构造函数需要哪些参数(依赖),然后去服务容器中查找并提供这些依赖的实例。
总结

依赖注入是ASP.NET Core 的灵魂。它通过 **“反向控制”的方式,让你的代码更加模块化、可测试和易于维护 **。记住,不要在类内部 new 一个依赖,而是通过构造函数让框架把它 “喂” 给你


12. MVC 自定义视图 (Custom View)

大白话解释

在 MVC 模式中,视图 (View) 就是负责展示数据给用户看的部分,通常就是 HTML 页面。自定义视图就是你自己创建一个 HTML 页面,并让它显示从控制器传来的数据

为什么需要它

默认的 return View(); 会寻找一个与 Action 方法同名的视图文件。但有时你可能想:

  • 用一个视图来展示不同 Action 的数据。
  • 把视图文件放在不同的文件夹结构下。
  • 明确指定要使用哪个视图。
代码示例

假设我们有一个 HomeController,里面有一个 Welcome Action。

1. 在控制器中指定视图名称

csharp

// HomeController.cs
public class HomeController : Controller
{
    public IActionResult Welcome(string name)
    {
        // 把从URL获取的 name 参数传递给视图
        ViewData["Name"] = name;

        // **自定义视图名称**:我们指定要使用名为 "Greet" 的视图,而不是 "Welcome"。
        return View("Greet"); 
    }
}

2. 创建对应的视图文件现在,你需要在 Views/Home/ 文件夹下创建一个名为 Greet.cshtml 的文件。

html

预览

<!-- Views/Home/Greet.cshtml -->
@{
    ViewData["Title"] = "Welcome";
}

<h1>@ViewData["Title"]</h1>
<p>Hello, @ViewData["Name"]! Welcome to our custom view.</p>

当你访问 http://localhost:5000/Home/Welcome?name=Alice 时,虽然 URL 是 Welcome,但页面会由 Greet.cshtml 渲染,并显示 "Hello, Alice!..."。

语法详解
  • return View("Greet");
    • View() 方法可以接受一个字符串参数,用来指定视图的名称。
    • 框架会默认在 Views/[ControllerName]/ 文件夹下寻找名为 Greet.cshtml 的文件。
    • 你也可以指定完整路径:return View("~/Views/Shared/Greet.cshtml"); (使用 ~ 表示应用根目录)。
总结

自定义视图让你在 **“哪个数据由哪个页面来展示”这个问题上拥有了更大的灵活性 **。


13. 强类型视图 (Strongly Typed View)

大白话解释

强类型视图就是让视图和一个具体的数据模型(Model)“绑定” 在一起。这样,在视图里你就可以像使用普通 C# 对象一样,通过 @Model.Property 的方式来访问数据,并且能获得编译时检查代码智能提示

这比使用 ViewData 或 ViewBag(后面会讲)更安全、更规范。

为什么需要它
  • 类型安全 (Type Safety):如果你在视图里写错了模型的属性名(比如把 Name 写成了 Nam),代码在编译时就会报错,而不是等到运行时才发现。
  • 智能提示 (IntelliSense):在 Visual Studio 中输入 @Model. 时,会自动弹出该模型所有可用的属性和方法。
  • 代码清晰 (Cleaner Code)@Model.Name 比 @ViewData["Name"] 更直观,更容易理解数据的来源和结构。
代码示例

我们来创建一个展示用户信息的强类型视图。

1. 创建模型类

csharp

// Models/User.cs
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

2. 在控制器中传递模型实例

csharp

// HomeController.cs
public class HomeController : Controller
{
    public IActionResult Profile()
    {
        // 创建一个 User 模型的实例
        var user = new User
        {
            Id = 1,
            Name = "Bob Smith",
            Email = "bob@example.com"
        };

        // **将整个模型对象传递给视图**
        return View(user);
    }
}

3. 创建强类型视图在 Views/Home/ 文件夹下创建 Profile.cshtml,并在文件顶部使用 @model 指令来指定模型类型。

html

预览

@model MyWebApp.Models.User <!-- 声明这是一个强类型视图,模型类型是 User -->

@{
    ViewData["Title"] = "User Profile";
}

<h1>@ViewData["Title"]</h1>

<div>
    <h4>@Model.Name</h4> <!-- 使用 @Model 访问模型实例 -->
    <hr />
    <dl class="row">
        <dt class="col-sm-2">Id:</dt>
        <dd class="col-sm-10">@Model.Id</dd>

        <dt class="col-sm-2">Name:</dt>
        <dd class="col-sm-10">@Model.Name</dd>

        <dt class="col-sm-2">Email:</dt>
        <dd class="col-sm-10">@Model.Email</dd>
    </dl>
</div>
语法详解
  • @model MyWebApp.Models.User
    • 这是关键@model 指令(注意是小写 m)告诉视图引擎,这个视图期望接收一个 MyWebApp.Models.User 类型的模型。
  • @Model
    • 在视图的其他地方,你可以使用 @Model(大写 M)来访问传递过来的那个具体的模型实例。
总结

尽可能地使用强类型视图。它是现代ASP.NET Core 开发的最佳实践,可以让你的代码更健壮、更易于维护。


14. ViewBag 和 ViewData

大白话解释

ViewBag 和 ViewData 都是用来在控制器和视图之间临时传递少量数据的 “信使”。它们非常相似,但语法略有不同。

  • ViewData:是一个 ViewDataDictionary 对象,你需要像操作字典一样,使用 ["Key"] 的方式来存取数据。
  • ViewBag:是一个 dynamic 动态类型对象,你可以像操作普通类的属性一样,使用 .Key 的方式来存取数据。

它们都只在当前请求中有效,一旦页面渲染完成,它们就失效了。

为什么需要它们

当你需要向视图传递一些简单的、非结构化的数据时(比如页面标题、一个下拉列表的选项、一条提示信息等),使用 ViewBag 或 ViewData 比创建一个专门的视图模型(ViewModel)要简单快捷。

代码示例

在控制器中设置数据

csharp

// HomeController.cs
public IActionResult Index()
{
    // 使用 ViewData
    ViewData["Message"] = "Hello from ViewData!";
    ViewData["CurrentTime"] = DateTime.Now;

    // 使用 ViewBag
    ViewBag.Greeting = "Hi there from ViewBag!";
    ViewBag.UserCount = 152;

    return View();
}

在视图中读取数据

html

预览

<!-- Views/Home/Index.cshtml -->
<h2>@ViewData["Message"]</h2>
<p>The current time is: @ViewData["CurrentTime"]</p>

<hr />

<h2>@ViewBag.Greeting</h2>
<p>Total users: @ViewBag.UserCount</p>
语法详解
  • 设置
    • ViewData["Key"] = value;
    • ViewBag.Key = value;
  • 读取
    • @ViewData["Key"]
    • @ViewBag.Key
  • 生命周期:两者都只在当前请求的生命周期内有效。如果你进行了页面跳转(RedirectToAction),它们里面的数据会丢失。
  • 类型转换:从 ViewData 中取出的数据是 object 类型,有时可能需要进行类型转换。而 ViewBag 是动态类型,会在运行时自动处理类型。
总结
特性ViewDataViewBag
类型ViewDataDictionarydynamic
语法ViewData["Key"]ViewBag.Key
类型安全否(需要手动转换)否(动态类型)
智能提示
生命周期当前请求当前请求

最佳实践:对于复杂的数据或需要类型安全的场景,优先使用强类型视图(ViewModel)。对于传递简单的、临时的消息或配置,ViewBag 或 ViewData 是很好的选择。ViewBag 因为语法更简洁,在现代开发中更受欢迎一些。


15. 视图模型 (ViewModel)

大白话解释

视图模型(ViewModel)是一个专门为视图(View)量身定制的 C# 类。它的唯一目的就是封装视图需要展示的所有数据

想象一下,你的一个页面需要同时显示:

  1. 当前登录用户的信息。
  2. 一个产品列表。
  3. 网站的一些全局设置(如网站标题、Logo 地址)。

这个页面的数据来源很复杂,不适合用单一的领域模型(如 Product)来传递。这时,你就可以创建一个 HomePageViewModel

为什么需要它
  1. 聚合数据 (Aggregate Data):将来自不同数据源的数据(多个领域模型、配置信息等)打包成一个单一的对象,方便传递给视图。
  2. 解耦 (Decoupling):让视图与业务逻辑(领域模型)解耦。视图只关心它的 ViewModel,不关心这些数据是怎么来的。
  3. 验证 (Validation):可以在 ViewModel 上直接添加数据验证特性(如 [Required]),实现对用户输入的验证。
  4. 类型安全 (Type Safety):ViewModel 配合强类型视图,提供了完整的类型安全和智能提示。
代码示例

假设我们要创建一个产品详情页,需要显示产品信息和评论列表。

1. 创建领域模型

csharp

// Models/Product.cs
public class Product { public int Id { get; set; } public string Name { get; set; } }
// Models/Review.cs
public class Review { public string Comment { get; set; } public string Reviewer { get; set; } }

2. 创建视图模型

csharp

// ViewModels/ProductDetailViewModel.cs
public class ProductDetailViewModel
{
    public Product Product { get; set; }
    public List<Review> Reviews { get; set; }
    public int ReviewCount => Reviews?.Count ?? 0; // 计算评论总数
}

3. 在控制器中构建并传递 ViewModel

csharp

// ProductController.cs
public class ProductController : Controller
{
    public IActionResult Detail(int id)
    {
        // 模拟从数据库获取数据
        var product = new Product { Id = id, Name = "Laptop" };
        var reviews = new List<Review>
        {
            new Review { Comment = "Great product!", Reviewer = "Alice" },
            new Review { Comment = "Works well.", Reviewer = "Bob" }
        };

        // **构建ViewModel**
        var viewModel = new ProductDetailViewModel
        {
            Product = product,
            Reviews = reviews
        };

        // **将ViewModel传递给视图**
        return View(viewModel);
    }
}

4. 创建强类型视图来使用 ViewModel

html

预览

@model MyWebApp.ViewModels.ProductDetailViewModel

<h1>@Model.Product.Name</h1>
<p>Product ID: @Model.Product.Id</p>

<h3>Reviews (@Model.ReviewCount)</h3>
@foreach (var review in @Model.Reviews)
{
    <div>
        <p>@review.Comment</p>
        <small>By @review.Reviewer</small>
    </div>
    <hr />
}
总结

视图模型是连接控制器和视图的最佳桥梁。它解决了 **“一个视图需要多种数据”的问题,是实现关注点分离构建健壮 Web 应用 ** 的关键所在。


16. 布局页面的使用 (Layout Pages)

大白话解释

布局页面就是网站的 **“母版页” 或 “模板”。它定义了网站中所有页面共享的结构 **,比如页眉(Header)、页脚(Footer)、导航栏(Navigation Bar)。

你可以把它想象成写信时使用的信纸模板。模板上已经有了固定的抬头和落款位置,你只需要在中间的空白区域(@RenderBody())填写具体的信内容即可。

为什么需要它
  • 代码复用:避免在每个页面都重复编写相同的 HTML 结构(如导航栏、页脚)。
  • 一致性:保证整个网站的外观和布局统一。
  • 易于维护:当需要修改网站的公共部分(比如更换 Logo)时,只需要修改一个布局文件即可,所有引用它的页面都会自动更新。
代码示例

1. 创建布局文件 _Layout.cshtml通常放在 Views/Shared/ 文件夹下,因为它是所有视图共享的。

html

预览

<!-- Views/Shared/_Layout.cshtml -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - MyWebApp</title>
    <!-- 引入Bootstrap等公共样式 -->
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
</head>
<body>
    <!-- 公共的导航栏 -->
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MyWebApp</a>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>

    <div class="container">
        <!-- 这里是每个内容页独有的内容会被渲染的地方 -->
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <!-- 公共的页脚 -->
    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2023 - MyWebApp - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>

    <!-- 引入公共的JavaScript -->
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>

    <!-- 预留一个区域给内容页添加自己的脚本 -->
    @RenderSection("Scripts", required: false)
</body>
</html>

2. 在内容页中指定使用该布局在你的视图文件(如 Views/Home/Index.cshtml)顶部,使用 Layout 属性来指定布局文件的路径。

html

预览

<!-- Views/Home/Index.cshtml -->
@{
    ViewData["Title"] = "Home Page";
    // 指定要使用的布局文件
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
语法详解
  • Layout = "~/Views/Shared/_Layout.cshtml";
    • 在视图的 Razor 代码块(@{ ... })中设置 Layout 属性,即可指定该视图使用的布局。路径通常使用 ~ 开头,表示应用程序的根目录。
  • @RenderBody()
    • 这是布局文件中的一个占位符。当一个视图使用此布局时,该视图的所有内容都会被渲染到 @RenderBody() 所在的位置。
总结

布局页面是实现网站结构复用和统一的关键。通过 Layout 属性和 @RenderBody() 占位符,你可以轻松地构建出具有一致外观的网站。


17. Sections

大白话解释

Sections(区域)是布局页面中的 **“预留插槽”。它们允许你从内容页向布局页面的特定区域 ** 注入 HTML 代码。

最常见的用途是:布局页在 <head> 标签或 </body> 标签前预留一个 Scripts 区域,这样每个内容页就可以根据自己的需要,添加特定的 CSS 或 JavaScript 文件。

为什么需要它
  • 灵活性:虽然所有页面共享一个布局,但它们可能有不同的脚本或样式需求。Sections 提供了这种灵活性。
  • 代码组织:将页面特定的脚本放在页面文件本身,而不是全部塞进布局文件,让代码更清晰、更易于维护。
代码示例

1. 在布局文件中定义一个 Section使用 @RenderSection("SectionName", required: false) 来定义一个区域。

html

预览

<!-- Views/Shared/_Layout.cshtml (部分代码) -->
<head>
    <!-- ... 其他公共样式 ... -->
    @RenderSection("Styles", required: false)
</head>
<body>
    <!-- ... @RenderBody() ... -->

    <!-- ... 公共脚本 ... -->
    @RenderSection("Scripts", required: false)
</body>
  • "Scripts":是 Section 的名称。
  • required: false:表示内容页不必须提供这个 Section。如果设为 true,而内容页没有提供,程序会报错。

2. 在内容页中填充 Section使用 @section SectionName { ... } 来填充布局中定义的区域。

html

预览

<!-- Views/Home/Index.cshtml -->
@{
    ViewData["Title"] = "Home Page";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<!-- 这部分内容会被渲染到 @RenderBody() -->
<h1>Welcome to the Home Page</h1>

<!-- **填充Scripts区域** -->
@section Scripts {
    <script>
        console.log("This is a script specific to the Home Page.");
        // 你也可以在这里引入页面特定的JS文件
        // <script src="~/js/home.js"></script>
    </script>
}

<!-- **填充Styles区域** -->
@section Styles {
    <style>
        h1 {
            color: rebeccapurple;
        }
    </style>
}
语法详解
  • @RenderSection("Scripts", required: false):在布局页中声明一个名为 "Scripts" 的区域,并指定它不是必需的。
  • @section Scripts { ... }:在内容页中定义要注入到名为 "Scripts" 的区域中的内容。
总结

Sections 是布局页面的有力补充。它们解决了 **“布局统一,但局部可变”** 的问题,让你能够为不同页面定制特定的脚本和样式。


18. _ViewStart.cshtml

大白话解释

_ViewStart.cshtml 是一个特殊的视图文件。它的代码会在每个视图(.cshtml 文件)被渲染之前自动执行

你可以把它想象成一个 “视图启动器” 或 “视图级别的构造函数”。

为什么需要它

最主要的用途就是为整个应用程序或某个文件夹下的所有视图统一设置 Layout 属性。这样,你就不需要在每个视图文件中都手动写一遍 Layout = "~/Views/Shared/_Layout.cshtml"; 了。

代码示例

在 Views/ 文件夹下创建一个 _ViewStart.cshtml 文件。

html

预览

<!-- Views/_ViewStart.cshtml -->
@{
    // 为所有视图设置默认的布局页面
    Layout = "~/Views/Shared/_Layout.cshtml";
}

效果:现在,你的所有视图(如 Views/Home/Index.cshtmlViews/Product/Detail.cshtml)都会自动使用 _Layout.cshtml 作为它们的布局,除非你在某个视图文件中明确地重新定义 Layout 属性来覆盖这个默认设置。

语法详解
  • 这个文件通常只包含一个简单的 Razor 代码块 @{ ... }
  • 因为它在每个视图之前执行,所以在这里设置的任何变量或属性都会影响后续的视图渲染。
  • 你也可以在特定的视图文件夹(如 Views/Admin/)中创建一个 _ViewStart.cshtml,它会覆盖根 Views/ 文件夹中的设置,从而为该文件夹下的视图应用不同的默认布局。
总结

_ViewStart.cshtml 是一个 **“省力工具”,它通过提供默认的视图设置(尤其是布局)**,极大地减少了代码重复。


19. _ViewImports.cshtml

大白话解释

_ViewImports.cshtml 也是一个特殊的视图文件。它的作用是为所有视图(.cshtml 文件)导入命名空间和标签助手,避免在每个视图文件顶部都重复编写 @using 和 @addTagHelper 指令。

你可以把它想象成一个 “视图全局头文件”。

为什么需要它
  • 减少重复代码:如果你的所有视图都需要使用 MyWebApp.Models 命名空间下的类,你只需要在 _ViewImports.cshtml 中写一次 @using MyWebApp.Models 即可,而不用在每个 .cshtml 文件里都写一遍。
  • 统一管理:当需要添加或移除一个标签助手时,只需修改这个文件。
代码示例

在 Views/ 文件夹下创建一个 _ViewImports.cshtml 文件。

html

预览

<!-- Views/_ViewImports.cshtml -->
@using MyWebApp
@using MyWebApp.Models
@using MyWebApp.ViewModels

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

效果

  • 所有视图现在都可以直接使用 MyWebApp.Models 和 MyWebApp.ViewModels 命名空间下的类,无需再 @using
  • @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 这行代码导入了ASP.NET Core 所有内置的标签助手(如 <form asp-action="...">),让你可以在所有视图中直接使用它们。
语法详解
  • @using MyWebApp.Models:导入 MyWebApp.Models 命名空间。
  • @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    • @addTagHelper:指令用于添加标签助手。
    • *:表示导入该程序集中的所有标签助手。
    • Microsoft.AspNetCore.Mvc.TagHelpers:是包含ASP.NET Core 内置标签助手的程序集名称。
总结

_ViewImports.cshtml 是另一个 **“省力工具”,它通过集中管理命名空间和标签助手的导入 **,让你的视图代码更加整洁。


20. 传统路由 (Conventional Routing)

大白话解释

传统路由是一种基于模板的 URL 匹配机制。它就像一个 **“交通警察”**,根据你在 Program.cs 中定义的 URL 模板,来决定一个进来的 URL 请求应该交给哪个控制器(Controller)的哪个方法(Action)来处理。

为什么需要它

它提供了一种简单、统一的方式来映射 URL 和服务器端的处理逻辑。用户可以通过一个有意义的 URL(如 http://example.com/Product/Detail/1)来访问特定资源,而开发者也无需为每个页面都手动编写复杂的匹配逻辑。

代码示例

在 Program.cs 中配置路由。

csharp

var builder = WebApplication.CreateBuilder(args);

// 添加MVC服务
builder.Services.AddControllersWithViews();

var app = builder.Build();

// ... 其他中间件 ...

// 启用路由中间件
app.UseRouting();

// ... 其他中间件,如授权 ...
app.UseAuthorization();

// 配置终结点 (Endpoints)
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default", // 路由的名称,用于在视图中生成URL
        pattern: "{controller=Home}/{action=Index}/{id?}" // 路由模板
    );
});

app.Run();
语法详解
  • app.UseRouting();:启用路由中间件,它负责从 URL 中解析出路由信息。
  • app.UseEndpoints(...);:配置终结点,即定义路由模板和它要映射到的处理程序(这里是 Controller 的 Action)。
  • endpoints.MapControllerRoute(...):为 MVC 控制器配置一个路由。
  • pattern: "{controller=Home}/{action=Index}/{id?}":这是核心
    • {controller}:一个占位符,匹配 URL 中的一段,并将其作为控制器的名称。框架会自动在其后加上 "Controller" 来查找对应的类(如 "Product" -> ProductController)。
    • {action}:一个占位符,匹配 URL 中的一段,并将其作为 Action 方法的名称。
    • {id}:一个占位符,匹配 URL 中的一段,通常用作资源的唯一标识符(如产品 ID)。
    • =Home / =Index:这是默认值。如果 URL 中没有提供对应的段,就使用这个默认值。例如,访问根路径 / 时,等同于访问 /Home/Index
    • ?:表示 id 这个段是可选的。URL 中可以有也可以没有。

如何工作:当一个请求到达,例如 http://localhost:5000/Product/Detail/101

  1. 路由中间件解析 URL。
  2. 它匹配到 {controller} = "Product", {action} = "Detail", {id} = "101"。
  3. 框架查找 ProductController 类,并调用其中的 Detail(string id) 方法,同时将 "101" 作为参数传递进去。
总结

传统路由通过一个灵活的 URL 模板,将 URL 路径段(如 /Product/Detail/101)自动映射到控制器(Controller)、方法(Action)和参数(Parameters),是构建 MVC 应用的基础。


这一部分我们学习了ASP.NET Core 中构建视图和处理 URL 的核心技术:

  • 布局页面、Sections、_ViewStart、_ViewImports:这些工具让你能够高效地组织和复用视图代码,构建出结构清晰、易于维护的用户界面。
  • 传统路由:它是连接用户输入的 URL 和服务器端业务逻辑的桥梁

掌握了这些,你就可以开始构建一个具有统一布局、复杂页面和友好 URL 的完整 Web 应用了。接下来,我们将继续学习属性路由、标记助手、模型绑定和验证等更高级的主题。

21. 属性路由 (Attribute Routing)

大白话解释

属性路由是一种更灵活、更直观的 URL 映射方式。它不像传统路由那样在 Program.cs 中定义一个全局的 “模板”,而是直接在控制器(Controller)和方法(Action)的上方,用 C# 特性(Attribute)来精确地指定这个 Action 对应的 URL 是什么。

如果说传统路由是一个 “交通警察”,那么属性路由就是给每个 “目的地”(Action)都挂上了一个清晰的 “门牌地址”。

为什么需要它
  • 精确控制:可以为每个 Action 定义完全不同的、任意结构的 URL,例如 api/products/123 或 blog/my-first-post
  • 可读性强:URL 的结构直接体现在代码上,一目了然,便于理解和维护。
  • 符合 RESTful 风格:特别适合构建 API,因为它能轻松实现 GET /api/usersPOST /api/usersPUT /api/users/5 这样的 URL。
代码示例

我们用属性路由改造一个 ProductController

csharp

// Controllers/ProductController.cs
using Microsoft.AspNetCore.Mvc;

// [Route("products")] // 为整个控制器添加路由前缀
[Route("api/[controller]")] // 更通用的写法,[controller]会自动替换为控制器名(Product)
public class ProductController : Controller
{
    // GET: /api/product
    [HttpGet]
    public IActionResult ListAll()
    {
        return Content("这是所有产品的列表。");
    }

    // GET: /api/product/5
    // [HttpGet("details/{id}")] // 完整路径: /api/product/details/5
    [HttpGet("{id}")] // 完整路径: /api/product/5
    public IActionResult GetById(int id)
    {
        return Content($"正在显示ID为 {id} 的产品详情。");
    }

    // GET: /api/product/special
    [HttpGet("special")] // 完整路径: /api/product/special
    public IActionResult GetSpecialOffer()
    {
        return Content("这是一个特别优惠的产品!");
    }
}
语法详解
  • [Route("...")]:这是核心特性。它可以用在控制器级别Action 级别
    • 控制器级别[Route("api/products")] 为该控制器下的所有 Action 添加了一个 URL 前缀。
    • Action 级别[HttpGet("{id}")] 定义了该 Action 的具体 URL 路径。最终的 URL 是控制器路由 + Action 路由
  • [HttpGet][HttpPost][HttpPut][HttpDelete]:这些特性不仅定义了 HTTP 方法(谓词),它们本身也是一种路由特性。[HttpGet] 等同于 [Route("")]
  • [controller] 和 [action] 令牌:在 [Route] 特性中,你可以使用 [controller] 和 [action] 作为占位符,它们会在运行时自动替换为控制器名和 Action 名(去掉 "Controller" 后缀)。这使得路由更加通用和易于维护。
  • {id}:这是一个路由参数占位符,它会捕获 URL 中对应位置的值,并将其作为参数传递给 Action 方法。
总结

属性路由通过在代码中直接标记 URL 路径,提供了无与伦比的灵活性和精确性。对于构建RESTful API或需要自定义 URL 结构的场景,它是首选。在现代ASP.NET Core 开发中,属性路由的使用非常普遍。


22. 使用包管理工具安装 Bootstrap

大白话解释

Bootstrap 是一个非常流行的前端 UI 框架,它提供了大量现成的 CSS 样式和 JavaScript 组件(如按钮、表单、导航栏、弹窗等),让你能快速构建出美观且响应式的网页。使用包管理工具(如 LibMan 或 npm)安装它,就像在手机应用商店里下载一个 App 一样方便。

为什么需要它
  • 快速开发:无需从零开始编写复杂的 CSS,直接使用 Bootstrap 的类就能美化页面。
  • 响应式设计:Bootstrap 内置的栅格系统能让你的网站在电脑、平板和手机上都有良好的显示效果。
  • 专业美观:由 Twitter 开发并维护,社区庞大,样式经过精心设计,非常专业。
如何使用 (以 LibMan 为例)

LibMan (Library Manager) 是 Visual Studio 内置的一个轻量级客户端库管理工具,非常适合ASP.NET Core 项目。

  1. 在项目上右键 -> 管理客户端库 (Manage Client-Side Libraries)
  2. 这会在项目根目录下创建一个 libman.json 文件。
  3. 编辑 libman.json 文件,指定要安装的库和存放位置。

libman.json 示例:

json

{
  "version": "1.0",
  "defaultProvider": "cdnjs", // 默认的包源,cdnjs是一个流行的CDN
  "libraries": [
    {
      "library": "twitter-bootstrap@5.3.0", // 要安装的库及其版本
      "destination": "wwwroot/lib/bootstrap" // 库文件要存放的目标文件夹
    },
    {
      "library": "jquery@3.6.0", // Bootstrap的某些组件依赖jQuery
      "destination": "wwwroot/lib/jquery"
    }
  ]
}
  1. 保存文件。Visual Studio 会自动根据配置从 CDN 下载 Bootstrap 和 jQuery,并将它们解压到 wwwroot/lib/ 目录下。

  2. 在布局文件中引用:现在,你可以在 _Layout.cshtml 中引用这些文件了。

    html

    预览

    <!-- Views/Shared/_Layout.cshtml -->
    <head>
        <!-- ... -->
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    </head>
    <body>
        <!-- ... @RenderBody() ... -->
    
        <script src="~/lib/jquery/dist/jquery.min.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
        <!-- ... -->
    </body>
    
总结

使用 LibMan 等包管理工具可以极大地简化前端库的获取和更新过程。它能确保团队成员使用的库版本一致,并将库文件整齐地组织在 wwwroot 目录中,方便引用。


23. 标记助手 (Tag Helpers)

大白话解释

标记助手(Tag Helpers)允许你在 Razor 视图(.cshtml 文件)中使用看起来像标准 HTML 元素,但带有 asp-* 属性的标签。这些标签在服务器端被ASP.NET Core 处理,最终生成标准的 HTML。

你可以把它想象成 **“智能 HTML 标签”**。它们让你能用更自然、更安全的方式来生成链接、表单、图片等元素,而无需编写复杂的 C# 代码或拼接字符串。

为什么需要它
  • 提升开发体验:代码更像 HTML,对前端开发者更友好。在 Visual Studio 中,它们也有智能提示和颜色高亮。
  • 增强可读性和可维护性<a asp-action="Index">Home</a> 比 @Html.ActionLink("Home", "Index") 更直观。
  • 类型安全:例如,asp-action 的值会在编译时进行检查,如果 Action 不存在,会给出警告。
  • 功能强大:能轻松处理复杂的场景,如表单提交、图片版本控制、环境切换等。
代码示例

最常用的标记助手是 AnchorTagHelper(用于生成链接)和 FormTagHelper(用于生成表单)。

html

预览

<!-- Views/Home/Index.cshtml -->
@{
    ViewData["Title"] = "Home Page";
}

<!-- 1. Anchor Tag Helper (生成链接) -->
<!-- 生成的HTML: <a href="/Home/Privacy">Privacy Policy</a> -->
<p>Please visit our <a asp-action="Privacy" asp-controller="Home">Privacy Policy</a>.</p>

<!-- 2. Form Tag Helper (生成表单) -->
<!-- 生成的HTML: <form action="/Product/Create" method="post"> ... </form> -->
<!-- 它还会自动添加一个防伪令牌 (Anti-Forgery Token) 来防止CSRF攻击 -->
<form asp-action="Create" asp-controller="Product" method="post">
    <input type="text" name="Name" />
    <button type="submit">Create</button>
</form>
语法详解
  • asp-action="Privacy":指定链接或表单提交的 Action 名称。
  • asp-controller="Home":指定链接或表单提交的 Controller 名称。如果不指定,默认使用当前视图所在的 Controller。
  • method="post":指定表单的 HTTP 提交方法。
  • 如何启用:标记助手是通过 _ViewImports.cshtml 文件中的 @addTagHelper 指令启用的。一个新的ASP.NET Core 项目通常已经配置好了:

    html

    预览

    @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
    
总结

标记助手是ASP.NET Core 视图开发中的革命性特性。它将服务器端逻辑无缝地融入 HTML,让 Razor 视图变得更加简洁、直观和强大


24. Image Tag Helper

大白话解释

Image Tag Helper 是一个专门用于处理 <img> 标签的标记助手。它最主要的功能是自动在图片 URL 后面添加一个版本哈希值

这样做的好处是可以有效地解决浏览器缓存问题

为什么需要它

当你更新网站上的一张图片(比如 logo.png)时,如果文件名没变,用户的浏览器可能因为缓存而不会去下载新图片,导致用户看到的还是旧的图片。通过在 URL 后添加一个唯一的哈希值(如 logo.png?v=abc123...),浏览器会认为这是一个全新的 URL,从而总是请求最新的图片。

代码示例

html

预览

<!-- Views/Home/Index.cshtml -->

<!-- 使用 Image Tag Helper -->
<img asp-src="~/images/logo.png" alt="My Logo" class="img-fluid">
语法详解
  • asp-src="~/images/logo.png"
    • asp-src 是 Image Tag Helper 的属性,用于指定图片的源路径。
    • ~ 符号代表应用程序的根目录。
  • 生成的 HTML:当页面被渲染时,ASP.NET Core 会计算 logo.png 文件的哈希值,并自动附加到 URL 后面。

    html

    预览

    <img src="/images/logo.png?v=qN3Z_9jC3X3..." alt="My Logo" class="img-fluid">
    
    每次 logo.png 文件内容改变,这个 v=... 值都会自动更新。
总结

Image Tag Helper 是一个解决静态资源缓存问题的 “神器”。它让你无需手动管理版本号,就能确保用户总是能加载到最新的图片资源。


25. Environment Tag Helper 帮助隔离生产和开发环境

大白话解释

Environment Tag Helper 允许你根据当前应用运行的环境(如开发环境 Development 或生产环境 Production)来条件性地渲染不同的 HTML 内容

这对于在开发时使用未压缩的、便于调试的脚本,而在生产时使用压缩的、优化过的脚本非常有用。

为什么需要它
  • 开发便利:在开发环境中加载完整的、未压缩的 CSS/JS 文件,方便调试。
  • 生产优化:在生产环境中加载压缩和合并后的文件,减少网络请求,提高页面加载速度。
  • 环境特定逻辑:可以在不同环境下显示不同的信息或加载不同的服务。
代码示例

在 _Layout.cshtml 文件中使用它来加载不同的脚本。

html

预览

<!-- Views/Shared/_Layout.cshtml -->
<environment include="Development">
    <!-- 在开发环境中,加载未压缩的、独立的文件 -->
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.js"></script>
</environment>

<environment exclude="Development">
    <!-- 在非开发环境(如Staging, Production)中,加载压缩后的文件 -->
    <script src="~/lib/jquery/dist/jquery.min.js" asp-fallback-src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js" asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
    <script src="~/js/site.min.js" asp-append-version="true" asp-fallback-src="~/js/site.js"></script>
</environment>
语法详解
  • <environment include="Development">:只有当当前环境是 "Development" 时,才渲染这个标签内的内容。
  • <environment exclude="Development">:只要当前环境不是 "Development",就渲染这个标签内的内容。
  • include 和 exclude 属性可以接受多个环境名称,用逗号分隔,例如 include="Development,Staging"
  • 如何设置环境:环境是通过 ASPNETCORE_ENVIRONMENT 环境变量来设置的,通常在 launchsettings.json (开发时) 或服务器上配置。
总结

Environment Tag Helper 是一个强大的环境隔离工具。它让你能够轻松地为开发和生产环境提供不同的资源和行为,从而优化开发流程和最终产品的性能。


26. 配置菜单导航信息 (使用 ViewBag)

大白话解释

配置菜单导航信息,就是在网站的布局页面(_Layout.cshtml)中创建一个导航栏。为了让导航栏知道当前用户正在访问哪个页面,并给对应菜单项添加 “激活”(active)样式,我们可以在控制器中通过 ViewBag 传递当前页面的信息。

为什么需要它
  • 用户体验:高亮显示当前所在的页面,让用户清楚自己在网站中的位置。
  • 动态样式:无需为每个视图手动编写 CSS 类,通过后端逻辑动态控制。
代码示例

1. 在控制器中设置当前页面名称

csharp

// Controllers/HomeController.cs
public class HomeController : Controller
{
    public IActionResult Index()
    {
        ViewBag.CurrentPage = "Home"; // 设置当前页面标记
        return View();
    }

    public IActionResult Privacy()
    {
        ViewBag.CurrentPage = "Privacy"; // 设置当前页面标记
        return View();
    }
}

2. 在布局文件中使用 ViewBag 动态添加样式

html

预览

<!-- Views/Shared/_Layout.cshtml -->
<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container">
        <a class="navbar-brand" asp-action="Index">MyApp</a>
        <div class="collapse navbar-collapse">
            <ul class="navbar-nav">
                <li class="nav-item">
                    <!-- 如果 ViewBag.CurrentPage 是 "Home",则添加 "active" 类 -->
                    <a class="nav-link @(ViewBag.CurrentPage == "Home" ? "active" : "")" asp-action="Index">Home</a>
                </li>
                <li class="nav-item">
                    <!-- 如果 ViewBag.CurrentPage 是 "Privacy",则添加 "active" 类 -->
                    <a class="nav-link @(ViewBag.CurrentPage == "Privacy" ? "active" : "")" asp-action="Privacy">Privacy</a>
                </li>
            </ul>
        </div>
    </div>
</nav>
语法详解
  • ViewBag.CurrentPage = "Home";:在控制器的 Action 方法中,我们向 ViewBag 添加一个名为 CurrentPage 的动态属性。
  • @(ViewBag.CurrentPage == "Home" ? "active" : ""):这是 Razor 语法中的三元运算符
    • @(...):用于在 HTML 中嵌入一段 C# 表达式。
    • ViewBag.CurrentPage == "Home":判断 ViewBag 中的 CurrentPage 属性是否等于字符串 "Home"。
    • ? "active" : "":如果条件为真,输出 "active" 字符串;否则,输出空字符串。
    • 这样,只有当前页面匹配的菜单项,其 <a> 标签才会被赋予 class="nav-link active",Bootstrap 会自动为其应用高亮样式。
总结

使用 ViewBag 是一种简单直接的方式来实现导航栏的动态高亮。它虽然简单,但在大型项目中可能会导致代码重复(每个 Action 都要设置 ViewBag)。


27. 配置菜单导航信息 (使用 ViewModel)

大白话解释

这是实现导航菜单高亮的更优雅、更健壮的方式。我们不再在每个控制器 Action 中重复设置 ViewBag,而是创建一个基础视图模型(Base ViewModel),让所有其他视图模型都继承它。然后在一个集中的地方(如过滤器)来填充这个导航信息。

为什么需要它
  • 代码复用:避免在每个 Action 中重复写 ViewBag.CurrentPage
  • 集中管理:导航逻辑集中在一个地方,修改起来更方便。
  • 类型安全:使用 ViewModel 的属性,而不是动态的 ViewBag,可以获得编译时检查。
代码示例

1. 创建基础视图模型

csharp

// ViewModels/BaseViewModel.cs
public class BaseViewModel
{
    public string CurrentPage { get; set; }
}

2. 创建具体的视图模型并继承它

csharp

// ViewModels/HomeIndexViewModel.cs
public class HomeIndexViewModel : BaseViewModel
{
    public string WelcomeMessage { get; set; }
}

3. 创建一个 Action 过滤器来填充 CurrentPage过滤器可以在 Action 执行前后自动执行一些逻辑。

csharp

// Filters/SetCurrentPageFilter.cs
public class SetCurrentPageFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // 从路由数据中获取控制器和Action的名称
        var controllerName = context.RouteData.Values["controller"].ToString();
        var actionName = context.RouteData.Values["action"].ToString();

        // 构建一个唯一的页面标识
        var currentPage = $"{controllerName}-{actionName}";

        // 检查ViewData.Model是否是BaseViewModel的实例
        if (context.Controller is Controller controller && controller.ViewData.Model is BaseViewModel baseModel)
        {
            baseModel.CurrentPage = currentPage;
        }
        else
        {
            // 作为备选方案,仍然可以使用ViewBag
            controller.ViewBag.CurrentPage = currentPage;
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Action执行后不需要做任何事
    }
}

4. 在 Program.cs 中注册并全局应用过滤器

csharp

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    // 将我们的过滤器添加到全局过滤器集合中
    options.Filters.Add<SetCurrentPageFilter>();
});

// ...

5. 在控制器中使用具体的 ViewModel

csharp

// Controllers/HomeController.cs
public class HomeController : Controller
{
    public IActionResult Index()
    {
        var model = new HomeIndexViewModel
        {
            WelcomeMessage = "Hello, World!"
            // 不再需要手动设置 CurrentPage
        };
        return View(model);
    }
}

6. 在布局文件中修改判断逻辑

html

预览

<!-- Views/Shared/_Layout.cshtml -->
<!-- ... -->
<a class="nav-link @(ViewBag.CurrentPage == "Home-Index" ? "active" : "")" asp-action="Index">Home</a>
<a class="nav-link @(ViewBag.CurrentPage == "Home-Privacy" ? "active" : "")" asp-action="Privacy">Privacy</a>
<!-- ... -->

注意:在这个例子中,因为过滤器同时设置了 ViewBag,所以布局文件的代码可以保持不变。如果只设置了 ViewModel,那么在布局文件中获取 CurrentPage 会稍微复杂一点,通常需要使用 @(Model as BaseViewModel)?.CurrentPage。为简化,这里我们保留了 ViewBag 的方式。

总结

使用ViewModel + 过滤器是实现导航高亮的最佳实践。它将关注点分离得非常好:控制器只负责准备业务数据,过滤器负责处理横切关注点(如导航状态),视图负责展示。


28. 使用表单标记助手创建学生视图

大白话解释

我们将创建一个页面,包含一个表单,用于创建新的学生信息。我们将使用强类型视图表单标记助手,让这个过程既简单又安全。

为什么需要它

这是 Web 开发中最常见的场景:用户输入数据 -> 提交到服务器 -> 服务器处理数据。使用表单标记助手可以极大地简化这个过程,并提供安全保障。

代码示例

1. 创建学生模型 (Student.cs)

csharp

// Models/Student.cs
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

2. 创建控制器 (StudentController.cs)

csharp

// Controllers/StudentController.cs
public class StudentController : Controller
{
    // GET: /Student/Create
    public IActionResult Create()
    {
        // 传递一个空的Student对象给视图,以启用强类型
        return View(new Student());
    }

    // POST: /Student/Create
    [HttpPost]
    [ValidateAntiForgeryToken] // 防止CSRF攻击
    public IActionResult Create(Student student)
    {
        if (ModelState.IsValid)
        {
            // 如果模型验证通过,这里可以保存student到数据库
            // _context.Add(student);
            // _context.SaveChanges();
            return RedirectToAction("Index"); // 保存成功后重定向
        }
        // 如果验证失败,返回表单视图,让用户重新输入
        return View(student);
    }
}

3. 创建强类型视图 (Create.cshtml)

html

预览

@model MyWebApp.Models.Student

@{
    ViewData["Title"] = "Create Student";
}

<h1>Create New Student</h1>

<form asp-action="Create"> <!-- 使用表单标记助手,它会自动生成正确的action和method -->
    <div class="form-group">
        <label asp-for="Name"></label> <!-- Label Tag Helper -->
        <input asp-for="Name" class="form-control" /> <!-- Input Tag Helper -->
    </div>

    <div class="form-group">
        <label asp-for="Age"></label>
        <input asp-for="Age" class="form-control" />
    </div>

    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
    </div>

    <div class="form-group">
        <input type="submit" value="Create" class="btn btn-primary" />
    </div>
</form>
语法详解
  • @model MyWebApp.Models.Student:声明这是一个强类型视图,模型是 Student
  • <form asp-action="Create">
    • asp-action="Create":告诉表单提交到当前控制器的 Create Action。
    • 因为 Create Action 有 [HttpPost] 特性,表单标记助手会自动生成 method="post"
    • 它还会自动生成一个隐藏的 <input> 字段,用于防伪令牌验证(与 [ValidateAntiForgeryToken] 配合)。
  • asp-for="Name":这是 Input Tag Helper 和 Label Tag Helper 的核心。
    • 它会自动生成 id 和 name 属性,值为 "Name"
    • 它会自动设置 type 属性(例如,对于 Email 属性,type 会是 "email")。
    • 对于 <label> 标签,它会自动生成 for="Name",并将标签文本设置为 "Name"(你也可以通过数据注解自定义)。
总结

使用强类型视图 + 表单标记助手是创建数据输入表单的黄金标准。它让你能以一种类型安全、简洁且安全的方式来构建表单。


29. 模型绑定 (Model Binding)

大白话解释

模型绑定是ASP.NET Core 的一个自动化机制。当一个请求到达(比如提交一个表单),它会自动从请求的URL 参数、表单数据、请求头等地方提取数据,然后将这些数据填充到 Action 方法的参数对象中

你可以把它想象成一个 **“自动装配工”**。当一个包裹(HTTP 请求)到达工厂,装配工(模型绑定器)会拆开包裹,把里面的零件(数据)自动组装成一个完整的产品(C# 对象),然后直接交给工人(Action 方法)使用。

为什么需要它
  • 简化代码:你不再需要手动从 Request.Form["Name"] 或 Request.Query["Id"] 中获取数据并手动创建和填充对象。
  • 类型转换:它会自动尝试将字符串数据转换为目标对象的属性类型(如将 "25" 转换为 int 类型的 Age 属性)。如果转换失败,会自动添加一个验证错误。
代码示例

我们继续使用上一节的 StudentController

csharp

// Controllers/StudentController.cs
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Student student) // <-- 模型绑定发生在这里
{
    // 当这个方法被调用时,ASP.NET Core已经:
    // 1. 看到方法需要一个 Student 类型的参数。
    // 2. 检查HTTP请求的表单数据(Form Data)。
    // 3. 找到名为 "Name", "Age", "Email" 的表单字段。
    // 4. 创建一个新的 Student 对象。
    // 5. 将表单字段的值赋给对象的对应属性(进行了类型转换)。
    // 6. 将填充好数据的 student 对象作为参数传递给 Create 方法。

    if (ModelState.IsValid)
    {
        // 因为模型绑定和验证已经完成,我们可以直接使用 student 对象
        // _context.Add(student);
        // _context.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(student);
}
语法详解
  • public IActionResult Create(Student student)
    • 模型绑定器会检查 student 参数。
    • 它会查找请求中所有与 Student 类的公共属性同名的数据源(如表单字段 name="Name")。
    • 它会处理类型转换。例如,如果表单中的 Age 字段是 "abc",无法转换为 int,模型绑定会失败,ModelState.IsValid 会变为 false
  • ModelState.IsValid:这是一个非常重要的属性,它表示模型绑定和模型验证是否都成功了。在处理数据之前,必须先检查它。
总结

模型绑定是ASP.NET Core MVC 的核心功能之一。它自动地、智能地将 HTTP 请求中的数据转换为你代码中可以直接使用的 C# 对象,极大地提高了开发效率。


30. 模型验证 (Model Validation)

大白话解释

模型验证是确保用户输入的数据符合你的预期规则的过程。这些规则(比如 “姓名不能为空”、“年龄必须大于 0”、“邮箱格式必须正确”)是你在 ** 模型类上通过数据注解(Data Annotations)** 来定义的。

当表单提交后,ASP.NET Core 会自动根据这些规则对绑定后的模型进行验证。

为什么需要它
  • 数据完整性:确保存入数据库的数据是有效的、符合业务规则的。
  • 用户体验:在服务器端验证失败后,将错误信息返回给用户,提示他们如何修正错误。
  • 安全性:防止恶意用户提交无效或有害的数据。
代码示例

1. 在模型上添加验证规则

csharp

// Models/Student.cs
using System.ComponentModel.DataAnnotations;

public class Student
{
    public int Id { get; set; }

    [Required(ErrorMessage = "姓名是必填项")] // 不能为空
    [Display(Name = "姓名")] // 用于生成友好的标签名
    public string Name { get; set; }

    [Range(18, 99, ErrorMessage = "年龄必须在18到99之间")] // 范围验证
    public int Age { get; set; }

    [Required(ErrorMessage = "邮箱是必填项")]
    [EmailAddress(ErrorMessage = "请输入有效的邮箱地址")] // 邮箱格式验证
    public string Email { get; set; }
}

2. 在视图中显示验证错误修改 Create.cshtml 视图,添加 ValidationMessage Tag Helper

html

预览

@model MyWebApp.Models.Student
<!-- ... -->
<form asp-action="Create">
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
        <!-- 添加验证消息标签助手 -->
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="Age"></label>
        <input asp-for="Age" class="form-control" />
        <span asp-validation-for="Age" class="text-danger"></span>
    </div>

    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
        <span asp-validation-for="Email" class="text-danger"></span>
    </div>

    <div class="form-group">
        <input type="submit" value="Create" class="btn btn-primary" />
    </div>
</form>

<!-- 引入jQuery和jQuery Validation插件,以实现客户端验证 -->
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

3. 在控制器中检查验证结果这部分代码在上一节已经展示过,关键就是 if (ModelState.IsValid)

csharp

// Controllers/StudentController.cs
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(Student student)
{
    if (ModelState.IsValid) // <-- 检查模型验证是否通过
    {
        // 验证通过,执行业务逻辑
        return RedirectToAction("Index");
    }
    // 验证失败,返回视图,并将带有错误信息的模型传递回去
    // 视图中的 asp-validation-for 会自动显示错误信息
    return View(student);
}
语法详解
  • [Required][Range][EmailAddress]:这些都是数据注解特性,用来定义验证规则。ErrorMessage 属性可以自定义验证失败时的提示信息。
  • <span asp-validation-for="Name" class="text-danger"></span>
    • 这个标签助手会在服务器端验证失败后,自动显示 Name 属性对应的错误信息。
    • class="text-danger" 是 Bootstrap 的类,让错误信息显示为红色。
  • ModelState.IsValid:一个布尔值,如果模型上的所有验证规则都满足,则为 true;否则为 false
  • 客户端验证:通过引入 jQuery Validation 插件,ASP.NET Core 还能利用这些数据注解自动生成客户端验证脚本,在用户点击 “提交” 按钮之前就进行验证,提供即时反馈,提升用户体验。
总结

模型验证是构建健壮 Web 应用最后一道防线。通过在模型上添加数据注解,并在控制器中检查 ModelState.IsValid,你可以轻松实现服务器端验证,并配合前端库实现客户端验证,确保数据的有效性和安全性。


这一部分我们学习了ASP.NET Core 中构建动态和交互式 Web 页面的关键技术:

  • 属性路由:提供了构建灵活 URL 的强大方式。
  • 包管理和标记助手:极大地提升了前端开发的效率和体验。
  • 模型绑定和验证:实现了从用户输入到服务器处理的完整、安全的数据流程。

掌握了这些,你就可以构建出功能完善、交互友好、数据安全的 Web 应用了。接下来,我们将继续学习Select 标签的验证、服务生命周期、以及数据访问等更高级的主题。

31. Select 标签的验证

大白话解释

select 标签(下拉列表)的验证和普通输入框的验证原理完全一样。最常见的需求是 **“请选择一项”,也就是禁止用户选择默认的 “-- 请选择 --” 选项 **。我们通过在模型属性上添加 [Required] 特性来实现。

为什么需要它

确保用户从下拉列表中选择了一个有效的选项,而不是直接提交默认的、无意义的选项。

代码示例

假设我们有一个 Student 模型,其中包含一个 GradeId 属性,需要从下拉列表中选择。

1. 在模型上添加 [Required] 验证

csharp

// Models/Student.cs
using System.ComponentModel.DataAnnotations;

public class Student
{
    // ... 其他属性 ...

    [Required(ErrorMessage = "请选择年级")] // <-- 核心:标记为必填
    [Display(Name = "年级")]
    public int GradeId { get; set; } // 用于存储选择的选项值

    // 用于在视图中显示下拉列表的选项
    public List<SelectListItem> GradeOptions { get; set; }
}

2. 在控制器中准备下拉列表数据

csharp

// Controllers/StudentController.cs
public IActionResult Create()
{
    var student = new Student();
    // 模拟从数据库获取年级列表
    student.GradeOptions = new List<SelectListItem>
    {
        new SelectListItem { Value = "", Text = "--请选择--" }, // 默认选项,Value为空
        new SelectListItem { Value = "1", Text = "一年级" },
        new SelectListItem { Value = "2", Text = "二年级" },
        new SelectListItem { Value = "3", Text = "三年级" }
    };
    return View(student);
}

3. 在视图中使用 select 标签助手

html

预览

@model MyWebApp.Models.Student
<!-- ... -->
<form asp-action="Create">
    <!-- ... 其他表单字段 ... -->

    <div class="form-group">
        <label asp-for="GradeId"></label>
        <!-- 使用 select 标签助手 -->
        <select asp-for="GradeId" asp-items="Model.GradeOptions" class="form-control"></select>
        <!-- 添加验证消息标签 -->
        <span asp-validation-for="GradeId" class="text-danger"></span>
    </div>

    <div class="form-group">
        <input type="submit" value="Create" class="btn btn-primary" />
    </div>
</form>
语法详解
  • [Required] 在 GradeId 上:这告诉验证系统,GradeId 这个整数属性不能为空。
  • new SelectListItem { Value = "", Text = "--请选择--" }:我们提供了一个默认选项,它的 Value 是空字符串 ""
  • 当表单提交时:
    • 如果用户没有选择,提交的 GradeId 值就是空字符串 ""
    • 模型绑定器尝试将 "" 转换为 int 类型的 GradeId,但会失败。
    • 因为转换失败,ModelState.IsValid 会变为 false,并且 [Required] 的错误信息会被触发。
    • 控制器中的 if (ModelState.IsValid) 检查失败,将带有错误信息的模型返回给视图。
    • 视图中的 <span asp-validation-for="GradeId"> 会自动显示 "请选择年级" 的错误提示。
总结

对 select 标签进行验证,核心就是在对应的模型属性上添加 [Required] 特性,并提供一个 **Value 为空的默认选项 **。这样,当用户未选择时,模型验证就会自然地失败。


32. 插件推荐使用

大白话解释

插件(在 Visual Studio 中通常称为 “扩展”)是可以安装到你的开发工具中的小工具,用来增强功能、提高开发效率和代码质量。

为什么需要它们
  • 提高效率:自动化一些重复的任务,如代码生成、格式化。
  • 保证质量:实时检查代码中的错误和不规范的写法。
  • 增强体验:提供更丰富的代码提示、导航和调试功能。
推荐插件列表
插件名称推荐理由主要功能
Resharper“神级” 代码分析和重构工具提供比 VS 更强大的代码分析、智能提示、代码重构建议(如重命名、提取方法)、代码格式规范化。强烈推荐,但是付费软件
SonarLint免费的代码质量检查工具实时检查代码,发现 bug、漏洞和代码异味(Code Smells),帮助你写出更健壮、更安全的代码。
PostmanAPI 开发的瑞士军刀虽然不是 VS 插件,但它是开发和测试 API 的必备工具。你可以用它来发送各种 HTTP 请求(GET, POST, PUT 等),查看响应,调试 API 接口。
SQL Server Object Explorer数据库管理集成Visual Studio 内置的功能,可以让你在 VS 中直接连接和管理 SQL Server 数据库,查看表结构、执行 SQL 查询,非常方便。
VS Color Theme Editor个性化你的 IDE如果你对默认的颜色主题不满意,可以用它来创建和修改自己喜欢的代码编辑器颜色主题。
总结

善用插件可以让你的开发工作事半功倍Resharper 和 Postman 是提升ASP.NET Core 开发效率的 “黄金组合”,强烈建议尝试。


33. AddSingleton, AddScoped, AddTransient

大白话解释

这三个方法是在依赖注入(DI)容器中注册服务时,用来指定服务实例生命周期的。简单来说,就是告诉框架:“这个服务的实例应该在什么时候被创建,以及它能存活多久?”

为什么需要它们

不同的服务有不同的特性和需求:

  • 有些服务是无状态的、线程安全的,可以被所有请求共享。
  • 有些服务是有状态的,只能在单个请求中被共享。
  • 有些服务非常轻量,每次使用时创建一个新的实例成本很低。

选择正确的生命周期对于性能、内存使用和正确性至关重要。

三种生命周期详解
生命周期AddSingletonAddScopedAddTransient
大白话解释“单例模式”:整个应用程序生命周期内,只创建一个实例,所有地方都共享这一个。“范围模式”:在单个 HTTP 请求的生命周期内,创建一个实例,该请求中的所有地方共享这一个。“瞬时模式”每次请求服务时,都创建一个全新的实例。
生活中的类比电影院的海报:一张海报挂在那里,所有观众(请求)看到的都是同一张。超市的储物柜:你(一个请求)拿到一个柜子,在你购物期间(请求处理期间),你可以反复打开这个柜子放东西。但下一个顾客(新请求)会拿到一个新的柜子。一次性筷子:每次用餐(每次注入),都给你一双全新的筷子。用完即弃。
何时使用适用于无状态、线程安全的服务。例如:配置读取器、日志服务、数据库连接工厂。适用于有状态的服务,且状态只在单个请求内有效。例如:数据库上下文(DbContext)、用户会话信息。适用于轻量级、无状态的服务。例如:简单的工具类、IDisposable对象。
代码示例services.AddSingleton<IMyService, MyService>();services.AddScoped<IMyService, MyService>();services.AddTransient<IMyService, MyService>();
代码示例

假设我们有一个简单的服务来生成 ID。

csharp

// IIdGenerator.cs
public interface IIdGenerator { int GetNextId(); }

// IdGenerator.cs
public class IdGenerator : IIdGenerator
{
    private int _id = 0;
    public int GetNextId() => ++_id;
}

注册不同的生命周期,观察结果:在一个请求中多次注入 IIdGenerator

  • AddSingleton:每次获取的 ID 都会递增(1, 2, 3...),因为整个应用共享一个实例。
  • AddScoped:在同一个请求内,ID 会递增,但在新的请求中会重置为 1
  • AddTransient:每次注入都会得到一个新实例,所以每次获取的 ID 都是 1
总结

选择正确的服务生命周期是依赖注入中的关键决策:

  • 需要跨请求共享状态或为了性能而复用实例时,用 AddSingleton
  • 需要在单个请求内共享状态时(最常见的是 DbContext),用 AddScoped
  • 服务是轻量级且无需共享时,用 AddTransient

34. 单层 Web 和多层 Web 的区别

大白话解释
  • 单层 Web (Single-Tier):所有代码(页面展示、业务逻辑、数据库操作)都混在一起,通常就在一个项目里,甚至一个文件里。
  • 多层 Web (Multi-Tier):将代码按照职责进行分层,通常分为表现层(UI)、业务逻辑层(BLL)和数据访问层(DAL)。每层只做自己的事,并通过清晰的接口与其他层通信。
为什么需要多层架构
  • 关注点分离 (Separation of Concerns):UI 层只关心页面展示,BLL 层只关心业务规则,DAL 层只关心数据如何存取。代码结构清晰,各司其职。
  • 易于维护和扩展:当需要修改业务规则时,你只需要修改 BLL 层,而不用动 UI 或 DAL。当需要更换数据库时,你只需要修改 DAL 层。
  • 便于团队协作:不同的开发人员可以同时开发不同的层,互不干扰。
  • 易于测试:你可以独立地对 BLL 层进行单元测试,而不需要启动整个 Web 服务器或连接真实的数据库。
代码示例对比

单层架构示例 (所有逻辑都在 Controller 里)

csharp

// Controllers/ProductController.cs
public class ProductController : Controller
{
    public IActionResult Index()
    {
        // 1. 直接在Controller里写数据访问逻辑
        var connectionString = "Server=...";
        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();
            var command = new SqlCommand("SELECT * FROM Products", connection);
            var reader = command.ExecuteReader();
            // ... 手动读取数据并转换成List<Product> ...
        }
        // 2. 直接在Controller里处理业务逻辑(如果有的话)
        // var discountedProducts = products.Where(p => p.Price > 100).ToList();
        
        return View(products);
    }
}

多层架构示例 (关注点分离)

  1. 表现层 (UI) - Controller

    csharp

    // Controllers/ProductController.cs
    public class ProductController : Controller
    {
        private readonly IProductService _productService;
    
        public ProductController(IProductService productService)
        {
            _productService = productService; // 注入业务逻辑层
        }
    
        public IActionResult Index()
        {
            // 3. Controller只调用业务逻辑层,不关心数据从哪来
            var products = _productService.GetAllProducts();
            return View(products);
        }
    }
    
  2. 业务逻辑层 (BLL) - Service

    csharp

    // Services/IProductService.cs
    public interface IProductService { List<Product> GetAllProducts(); }
    
    // Services/ProductService.cs
    public class ProductService : IProductService
    {
        private readonly IProductRepository _productRepository;
    
        public ProductService(IProductRepository productRepository)
        {
            _productRepository = productRepository; // 注入数据访问层
        }
    
        public List<Product> GetAllProducts()
        {
            // 2. 在这里处理业务逻辑
            var products = _productRepository.GetAll();
            // var discountedProducts = products.Where(p => p.IsActive).ToList();
            return products;
        }
    }
    
  3. 数据访问层 (DAL) - Repository

    csharp

    // Repositories/IProductRepository.cs
    public interface IProductRepository { List<Product> GetAll(); }
    
    // Repositories/ProductRepository.cs
    public class ProductRepository : IProductRepository
    {
        // 1. 在这里处理所有数据库操作
        public List<Product> GetAll()
        {
            // 使用EF Core或其他ORM来获取数据
            // return _context.Products.ToList();
            return new List<Product>(); // 模拟返回
        }
    }
    
总结

多层架构是构建大型、复杂、可维护 Web 应用的标准实践。它通过分层将复杂问题分解,使得代码更清晰、更健壮、更易于长期维护。


35. DbContext

大白话解释

DbContext 是 Entity Framework Core (EF Core) 中的核心类。你可以把它想象成应用程序和数据库之间的 “桥梁” 或 “管家”。它负责:

  1. 管理实体与数据库表的映射
  2. 提供对数据库的 CRUD(增删改查)操作
  3. 跟踪实体的状态变化(新增、修改、删除)。
  4. 将所有更改一次性提交到数据库(事务)。
为什么需要它

DbContext 是使用 EF Core 进行数据库操作的入口点。它抽象了底层的数据库访问细节,让你可以用面向对象的方式(操作 C# 对象)来和数据库交互,而不用手动编写大量的 SQL 语句。

代码示例

1. 创建你的 AppDbContext 类

csharp

// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyWebApp.Models;

public class AppDbContext : DbContext
{
    // 将DbContext注入到服务容器时需要这个构造函数
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    // DbSet<T> 代表数据库中的一张表
    // 例如,这个属性代表 "Products" 表
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
}

2. 在 Program.cs 中注册 DbContext

csharp

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// ...

// 注册AppDbContext,并配置使用SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// ...

var app = builder.Build();

3. 在 appsettings.json 中配置数据库连接字符串

json

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyWebAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

4. 在 Controller 中使用 DbContext

csharp

// Controllers/ProductController.cs
public class ProductController : Controller
{
    private readonly AppDbContext _context;

    public ProductController(AppDbContext context)
    {
        _context = context; // 通过依赖注入获取DbContext实例
    }

    public IActionResult Index()
    {
        // 使用LINQ查询数据库,就像操作内存中的列表一样
        var products = _context.Products.ToList();
        return View(products);
    }
}
语法详解
  • public class AppDbContext : DbContext:自定义的 DbContext 必须继承自 EF Core 的 DbContext 基类。
  • public DbSet<Product> Products { get; set; }:每个 DbSet<TEntity> 属性都对应数据库中的一个表。当你查询 _context.Products 时,EF Core 会生成 SELECT * FROM Products 的 SQL。
  • builder.Services.AddDbContext<AppDbContext>(...):这是将 DbContext 注册到依赖注入容器中的标准方式。
  • options.UseSqlServer(...):指定 DbContext 使用 SQL Server 作为数据库 provider。
  • builder.Configuration.GetConnectionString("DefaultConnection"):从 appsettings.json 文件中读取名为 DefaultConnection 的连接字符串。
总结

DbContext 是 EF Core 的 **“心脏”。它将你的领域模型(C# 类)和数据库表连接起来,是进行所有数据操作的中心枢纽 **。


36. 使用 SQL Server

大白话解释

使用 SQL Server 就是将你的ASP.NET Core 应用程序连接到微软的 SQL Server 数据库,并利用 EF Core 来创建、读取、更新和删除数据。

为什么需要它

SQL Server 是一个功能强大、稳定可靠的关系型数据库管理系统(RDBMS),广泛用于企业级应用开发。将它与ASP.NET Core 结合,可以构建出数据驱动的、高性能的 Web 应用。

如何使用

这个过程在上一节 DbContext 中已经基本涵盖,主要包括以下几个步骤:

  1. 安装必要的 NuGet 包:在你的项目中,需要安装 EF Core 的 SQL Server provider。

    bash

    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    
  2. 配置连接字符串:在 appsettings.json 文件中添加你的 SQL Server 数据库连接字符串。

    json

    "ConnectionStrings": {
      "DefaultConnection": "Server=Your_Server_Name;Database=Your_Db_Name;User Id=Your_Username;Password=Your_Password;"
    }
    
    • 对于本地开发,通常使用 LocalDB,连接字符串更简单:"Server=(localdb)\\mssqllocaldb;Database=MyWebAppDb;Trusted_Connection=True;"
  3. 在 Program.cs 中注册 DbContext 并指定 SQL Server

    csharp

    builder.Services.AddDbContext<AppDbContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
    
  4. 在代码中通过 DbContext 操作数据:一旦配置完成,你就可以在任何注入了 AppDbContext 的地方,使用 LINQ 来操作数据库了。

    csharp

    // 读取
    var products = await _context.Products.Where(p => p.Price > 100).ToListAsync();
    
    // 创建
    var newProduct = new Product { Name = "New Product", Price = 199.99 };
    _context.Products.Add(newProduct);
    await _context.SaveChangesAsync();
    
    // 更新
    var productToUpdate = await _context.Products.FindAsync(id);
    if (productToUpdate != null)
    {
        productToUpdate.Price = 299.99;
        await _context.SaveChangesAsync();
    }
    
    // 删除
    var productToDelete = await _context.Products.FindAsync(id);
    if (productToDelete != null)
    {
        _context.Products.Remove(productToDelete);
        await _context.SaveChangesAsync();
    }
    
总结

ASP.NET Core 应用连接到 SQL Server 主要分为三步安装驱动包配置连接字符串在 Program.cs 中注册 DbContext。之后,你就可以通过 DbContext 对象,使用直观的 LINQ 语法来操作数据库了。


37. 仓储模式 (Repository Pattern)

大白话解释

仓储模式是一种设计模式,它为每个实体(Entity)创建一个 **“仓储”(Repository)类 **。这个类封装了对该实体所有的数据库操作(CRUD)。你可以把它想象成每个实体都有一个专门的 “仓库管理员”,你要对这个实体的数据做任何操作,都只和它的 “仓库管理员” 打交道,而不用关心货物(数据)是如何被存放在货架(数据库表)上的。

为什么需要它
  1. 进一步解耦:它在业务逻辑层(Service)和数据访问层(EF Core)之间又增加了一层抽象。业务逻辑层不再直接依赖 DbContext,而是依赖更抽象的 IRepository 接口。
  2. 简化业务逻辑:将复杂的查询逻辑封装在 Repository 中,使业务逻辑层的代码更简洁。
  3. 便于单元测试:因为业务逻辑层依赖的是接口,所以在测试时,我们可以轻松地用一个内存中的 “假仓库”(Mock Repository)来替代真实的数据库操作,从而实现快速、独立的单元测试。
代码示例

我们为 Product 实体创建一个仓储。

1. 创建通用的 IRepository 接口为了代码复用,通常先定义一个通用接口。

csharp

// Repositories/IRepository.cs
public interface IRepository<T> where T : class
{
    IEnumerable<T> GetAll();
    T GetById(int id);
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
}

2. 创建 ProductRepository

csharp

// Repositories/ProductRepository.cs
public class ProductRepository : IRepository<Product>
{
    private readonly AppDbContext _context;

    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public IEnumerable<Product> GetAll() => _context.Products.ToList();

    public Product GetById(int id) => _context.Products.Find(id);

    public void Add(Product product) => _context.Products.Add(product);

    public void Update(Product product) => _context.Products.Update(product);

    public void Delete(int id)
    {
        var product = _context.Products.Find(id);
        if (product != null)
        {
            _context.Products.Remove(product);
        }
    }
}

3. 在 Program.cs 中注册仓储

csharp

// Program.cs
builder.Services.AddScoped<IRepository<Product>, ProductRepository>();

4. 在业务逻辑层(Service)中使用仓储

csharp

// Services/ProductService.cs
public class ProductService : IProductService
{
    private readonly IRepository<Product> _productRepository;

    public ProductService(IRepository<Product> productRepository)
    {
        _productRepository = productRepository;
    }

    public List<Product> GetAllProducts()
    {
        return _productRepository.GetAll().ToList();
    }
}
总结

仓储模式是实现业务逻辑与数据访问彻底解耦的关键一步。它为每个实体提供了一个清晰的、面向对象的数据操作接口,使得代码更易于维护和测试。在复杂的企业级应用中,这是一种非常推荐的最佳实践。


38. 迁移功能 (Migrations)

大白话解释

迁移(Migrations)是 EF Core 提供的一个强大工具,它可以自动地将你对 C# 实体类(领域模型)的更改,同步到数据库的表结构中。你不需要手动去写 ALTER TABLE 这样的 SQL 语句。

你可以把它想象成数据库的 “版本控制”。每次你修改了模型,就创建一个 “迁移”,记录下这次结构变更。然后,你可以 “应用” 这个迁移,EF Core 会自动生成并执行相应的 SQL 来更新数据库。

为什么需要它
  • 自动化:避免了手动编写和执行复杂、易错的数据库结构变更脚本。
  • 团队协作:迁移文件可以提交到版本控制系统(如 Git),团队成员可以轻松地同步数据库结构。
  • 可追溯:每一次数据库结构的变更都有记录,清晰地展示了数据库的演进历史。
  • 安全可逆:你可以轻松地回滚到任何一个 previous 的数据库版本。
如何使用 (使用 Package Manager Console)

假设你已经创建了 Product 模型和 AppDbContext

  1. 打开 Package Manager Console (PMC):在 Visual Studio 中,选择 视图(View) -> 其他窗口(Other Windows) -> 程序包管理器控制台(Package Manager Console)

  2. 创建第一次迁移:运行 Add-Migration 命令,并给这次迁移起一个有意义的名字,比如 "InitialCreate"。

    powershell

    Add-Migration InitialCreate
    

    发生了什么?

    • EF Core 会比较你的实体类和数据库当前的结构(如果数据库不存在,则认为是空)。
    • 它会发现需要创建 Products 表。
    • 它会在项目中创建一个 Migrations 文件夹,并生成一个带有时间戳和名称的 C# 文件(如 20231027100000_InitialCreate.cs)。这个文件包含了 Up() (应用变更) 和 Down() (回滚变更) 两个方法。
  3. 应用迁移到数据库:运行 Update-Database 命令。

    powershell

    Update-Database
    

    发生了什么?

    • EF Core 会执行 InitialCreate.cs 文件中的 Up() 方法。
    • 它会自动创建数据库(如果不存在)和 Products 表。
    • 它还会创建一个 __EFMigrationsHistory 表,用来记录哪些迁移已经被应用。
  4. 后续修改模型后

    • 修改 Product.cs (例如,添加一个 Description 属性)。
    • 创建新的迁移:Add-Migration AddDescriptionToProduct
    • 应用到数据库:Update-Database
总结

迁移是 EF Core 中管理数据库 schema(结构)变更的首选方式。它通过简单的命令,实现了从代码模型到数据库表结构的自动化同步,极大地简化了开发流程。


39. 种子数据 (Seeding Data)

大白话解释

种子数据就是在数据库初始化或迁移时,自动向表中插入一些预设的、基础的数据。这些数据是应用程序正常运行所必需的,比如:角色列表(“管理员”、“普通用户”)、系统配置项、默认分类等。

为什么需要它
  • 环境一致性:确保每个开发者的本地数据库、测试环境和生产环境在启动时都有相同的基础数据。
  • 简化开发:不需要手动向数据库中插入基础数据,新成员加入项目时,只需运行 Update-Database 即可获得一个可用的数据库。
  • 自动化部署:在应用发布到新环境时,种子数据可以自动部署,确保系统开箱即用。
如何使用 (在迁移中)

这是最常用和推荐的方式。

  1. 创建一个新的迁移,但不做任何模型变更。

    powershell

    Add-Migration SeedInitialData
    
  2. 编辑生成的迁移文件。在 Up() 方法中,使用 migrationBuilder.InsertData() 来添加数据。

    csharp

    // Migrations/20231027110000_SeedInitialData.cs
    public partial class SeedInitialData : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.InsertData(
                table: "Categories", // 要插入数据的表名
                columns: new[] { "Id", "Name", "Description" }, // 列名
                values: new object[] { 1, "电子产品", "Electronic devices and accessories" } // 值
            );
    
            migrationBuilder.InsertData(
                table: "Categories",
                columns: new[] { "Id", "Name", "Description" },
                values: new object[] { 2, "Books", "Paperback and hardcover books" }
            );
        }
    
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            // 在回滚时,删除这些数据
            migrationBuilder.DeleteData(
                table: "Categories",
                keyColumn: "Id",
                keyValue: 1
            );
    
            migrationBuilder.DeleteData(
                table: "Categories",
                keyColumn: "Id",
                keyValue: 2
            );
        }
    }
    
  3. 应用迁移

    powershell

    Update-Database
    

    运行后,Categories 表中就会自动有这两条数据了。

总结

种子数据是确保数据库在不同环境中保持初始状态一致性的好方法。通过在迁移中插入数据,你可以将数据初始化和结构变更绑定在一起,实现完全自动化的数据库部署。


40. 同步领域模型和数据库架构

大白话解释

同步领域模型和数据库架构,就是确保你的 C# 实体类(领域模型)和数据库中的表结构(数据库架构)始终保持一致。当你修改了代码中的模型时,数据库也应该随之更新。

为什么需要它

如果模型和数据库结构不同步,应用程序就会出错。例如,你在代码中给 Product 类添加了一个 Description 属性,但数据库的 Products 表没有这个列,那么当你尝试访问 product.Description 时,程序就会抛出异常。

如何同步 (最佳实践)

使用我们刚刚学过的 ** 迁移(Migrations)** 功能,就是同步两者的标准和最佳实践。

同步流程如下:

  1. 修改领域模型:在你的 C# 代码中,添加、删除或修改实体类的属性。

    csharp

    // 给Product类添加Description属性
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Description { get; set; } // <-- 新增属性
    }
    
  2. 创建迁移:在 Package Manager Console 中运行 Add-Migration 命令,生成一个记录这次变更的迁移文件。

    powershell

    Add-Migration AddDescriptionToProduct
    
  3. 应用迁移:运行 Update-Database 命令,EF Core 会读取迁移文件,并执行相应的 SQL(如 ALTER TABLE Products ADD Description nvarchar(max) NULL)来更新数据库结构。

    powershell

    Update-Database
    

完成以上三步后,你的领域模型和数据库架构就再次同步了。

总结

使用迁移(Migrations)是同步领域模型和数据库架构的唯一正确方法。它提供了一个安全、可重复、可审计的方式来管理数据库结构的演变,是现代.NET 开发中不可或缺的一部分。


这一部分我们学习了ASP.NET Core 中与数据处理和架构设计相关的核心概念:

  • Select 标签验证:确保用户从下拉列表中选择了有效选项。
  • 服务生命周期:理解 SingletonScopedTransient 的区别,是正确使用依赖注入的关键。
  • 多层架构:通过分层(UI, BLL, DAL)让代码更清晰、更易于维护。
  • DbContext:EF Core 的核心,是应用与数据库交互的桥梁。
  • 仓储模式:进一步解耦业务逻辑和数据访问,便于测试。
  • 迁移和种子数据:自动化地同步代码模型和数据库结构,并初始化基础数据。

掌握了这些,你就具备了构建一个功能完善、架构清晰、数据驱动的ASP.NET Core 应用的能力。接下来,我们将继续学习文件上传、错误处理、日志记录和部署等高级主题。

41. MVC 上传文件

大白话解释

MVC 上传文件,就是在网页上放一个 “选择文件” 的输入框,让用户可以从自己的电脑里选择一个文件,然后点击 “上传” 按钮,把这个文件发送到服务器并保存下来。

为什么需要它

文件上传是 Web 应用非常常见的功能,例如:

  • 用户上传头像或个人照片。
  • 管理员上传产品图片。
  • 用户上传简历或其他文档。
代码示例

我们来创建一个允许用户上传头像的功能。

1. 创建一个简单的视图模型

csharp

// ViewModels/FileUploadViewModel.cs
public class FileUploadViewModel
{
    [Display(Name = "请选择头像")]
    [Required(ErrorMessage = "请选择一个文件")]
    [DataType(DataType.Upload)]
    public IFormFile Avatar { get; set; }
}
  • IFormFile 是ASP.NET Core 中用来表示上传文件的特殊类型。

2. 创建控制器

csharp

// Controllers/UploadController.cs
public class UploadController : Controller
{
    private readonly IWebHostEnvironment _hostingEnvironment;

    public UploadController(IWebHostEnvironment hostingEnvironment)
    {
        // 注入IWebHostEnvironment以获取Web根目录路径
        _hostingEnvironment = hostingEnvironment;
    }

    // GET: /Upload/Avatar
    public IActionResult Avatar()
    {
        return View();
    }

    // POST: /Upload/Avatar
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Avatar(FileUploadViewModel model)
    {
        if (ModelState.IsValid)
        {
            // 检查是否有文件上传
            if (model.Avatar != null && model.Avatar.Length > 0)
            {
                // 1. 获取上传文件的扩展名
                var fileExtension = Path.GetExtension(model.Avatar.FileName);

                // 2. 为了防止文件名冲突,生成一个唯一的文件名
                var uniqueFileName = Guid.NewGuid().ToString() + fileExtension;

                // 3. 获取服务器上保存文件的路径
                // _hostingEnvironment.WebRootPath 是 wwwroot 文件夹的路径
                var uploadsFolder = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
                
                // 如果文件夹不存在,就创建它
                if (!Directory.Exists(uploadsFolder))
                {
                    Directory.CreateDirectory(uploadsFolder);
                }

                // 4. 拼接完整的文件保存路径
                var filePath = Path.Combine(uploadsFolder, uniqueFileName);

                // 5. 将上传的文件流保存到服务器的文件中
                using (var fileStream = new FileStream(filePath, FileMode.Create))
                {
                    await model.Avatar.CopyToAsync(fileStream);
                }

                // 6. 保存成功,返回一个成功提示
                ViewBag.Message = "文件上传成功!保存路径:" + filePath;
                return View();
            }
        }

        // 如果模型验证失败或没有文件,返回视图并显示错误
        return View(model);
    }
}

3. 创建视图

html

预览

@model MyWebApp.ViewModels.FileUploadViewModel

@{
    ViewData["Title"] = "上传头像";
}

<h1>上传头像</h1>

<form asp-action="Avatar" enctype="multipart/form-data">
    <div class="form-group">
        <label asp-for="Avatar"></label>
        <input asp-for="Avatar" class="form-control-file" />
        <span asp-validation-for="Avatar" class="text-danger"></span>
    </div>
    <div class="form-group">
        <input type="submit" value="上传" class="btn btn-primary" />
    </div>
</form>

@if (ViewBag.Message != null)
{
    <div class="alert alert-success">@ViewBag.Message</div>
}
  • 关键语法<form ... enctype="multipart/form-data">
    • 当表单包含文件上传时,enctype 属性必须设置为 multipart/form-data。否则,服务器将无法正确接收文件数据。
总结

实现文件上传的核心步骤是:

  1. 在视图中,使用 enctype="multipart/form-data" 的 <form> 和 type="file" 的 <input>
  2. 在控制器的 [HttpPost] 方法中,使用 IFormFile 类型的参数来接收文件。
  3. 使用 IWebHostEnvironment 获取服务器上的保存路径(通常是 wwwroot 文件夹下)。
  4. 生成一个唯一的文件名,防止覆盖。
  5. 使用 await file.CopyToAsync(stream) 将文件内容保存到服务器。

42. MVC 上传多个文件

大白话解释

上传多个文件和上传单个文件非常类似,只是允许用户一次选择并上传多个文件

为什么需要它
  • 批量上传图片到相册。
  • 批量上传文档到云盘。
代码示例

我们修改一下上一节的例子,使其支持多文件上传。

1. 修改视图模型使用 List<IFormFile> 或 IEnumerable<IFormFile>

csharp

// ViewModels/MultipleFileUploadViewModel.cs
public class MultipleFileUploadViewModel
{
    [Display(Name = "请选择图片")]
    [Required(ErrorMessage = "请至少选择一个文件")]
    public List<IFormFile> Photos { get; set; }
}

2. 修改控制器在 [HttpPost] 方法中,对文件列表进行循环处理。

csharp

// Controllers/UploadController.cs
// ... (省略IWebHostEnvironment的注入) ...

// GET: /Upload/Photos
public IActionResult Photos()
{
    return View();
}

// POST: /Upload/Photos
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Photos(MultipleFileUploadViewModel model)
{
    if (ModelState.IsValid)
    {
        // 检查是否有文件,并且至少上传了一个
        if (model.Photos != null && model.Photos.Count > 0)
        {
            var uploadsFolder = Path.Combine(_hostingEnvironment.WebRootPath, "photos");
            Directory.CreateDirectory(uploadsFolder);

            foreach (var photo in model.Photos)
            {
                // 跳过空文件
                if (photo.Length == 0) continue;

                var uniqueFileName = Guid.NewGuid().ToString() + Path.GetExtension(photo.FileName);
                var filePath = Path.Combine(uploadsFolder, uniqueFileName);

                using (var fileStream = new FileStream(filePath, FileMode.Create))
                {
                    await photo.CopyToAsync(fileStream);
                }
            }

            ViewBag.Message = "所有文件上传成功!";
            return View();
        }
    }

    return View(model);
}

3. 修改视图在 <input> 标签上添加 multiple 属性。

html

预览

@model MyWebApp.ViewModels.MultipleFileUploadViewModel

@{
    ViewData["Title"] = "上传图片";
}

<h1>上传图片</h1>

<form asp-action="Photos" enctype="multipart/form-data">
    <div class="form-group">
        <label asp-for="Photos"></label>
        <input asp-for="Photos" class="form-control-file" multiple />
        <small class="form-text text-muted">按住Ctrl键可以选择多个文件。</small>
        <span asp-validation-for="Photos" class="text-danger"></span>
    </div>
    <div class="form-group">
        <input type="submit" value="上传" class="btn btn-primary" />
    </div>
</form>

@if (ViewBag.Message != null)
{
    <div class="alert alert-success">@ViewBag.Message</div>
}
  • 关键语法<input ... multiple />
    • multiple 属性允许用户在文件选择器对话框中选择多个文件。
总结

实现多文件上传的核心改动是:

  1. 视图模型中的属性类型改为 List<IFormFile>
  2. 视图中的 <input type="file"> 添加 multiple 属性。
  3. 控制器的 [HttpPost] 方法中,通过 foreach 循环遍历并保存每一个文件。

43. 处理 HttpPost 的 Edit 操作方法

大白话解释

处理 HttpPost 的 Edit 方法,就是当用户在 “编辑” 页面修改了数据并点击 “保存” 按钮后,服务器端如何接收、验证并更新这些数据到数据库。

为什么需要它

这是 Web 应用中最核心的数据更新场景。用户必须能够修改已存在的信息。

代码示例

我们以编辑 Product 为例。

1. 创建或使用 Product 模型确保模型上有验证特性。

csharp

public class Product
{
    public int Id { get; set; }
    [Required] public string Name { get; set; }
    [Range(0.01, double.MaxValue)] public decimal Price { get; set; }
}

2. 控制器中的 Edit 方法通常需要两个 Edit 方法:一个 [HttpGet] 用于显示表单,一个 [HttpPost] 用于处理提交。

csharp

// Controllers/ProductController.cs
public class ProductController : Controller
{
    private readonly AppDbContext _context;

    public ProductController(AppDbContext context) => _context = context;

    // GET: /Product/Edit/5
    public async Task<IActionResult> Edit(int? id)
    {
        if (id == null) return NotFound();
        var product = await _context.Products.FindAsync(id);
        if (product == null) return NotFound();
        return View(product);
    }

    // POST: /Product/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Edit(int id, [Bind("Id,Name,Price")] Product product)
    {
        // 1. 检查ID是否匹配
        if (id != product.Id) return NotFound();

        if (ModelState.IsValid)
        {
            try
            {
                // 2. 标记实体为已修改
                _context.Update(product);
                // 3. 保存更改到数据库
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                // 4. 处理并发冲突(例如,在你编辑时,别人已经删除了该记录)
                if (!ProductExists(product.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw; // 其他并发错误,向上抛出
                }
            }
            // 5. 保存成功,重定向到列表页
            return RedirectToAction(nameof(Index));
        }
        // 6. 验证失败,返回表单视图,显示错误信息
        return View(product);
    }

    private bool ProductExists(int id) => _context.Products.Any(e => e.Id == id);
}

3. Edit 视图视图应该是一个强类型视图,并且表单的 method 是 post

html

预览

@model MyWebApp.Models.Product

@{
    ViewData["Title"] = "Edit Product";
}

<h1>Edit Product</h1>

<form asp-action="Edit">
    <input type="hidden" asp-for="Id" /> @* Id通常放在隐藏域中 *@
    <div class="form-group">
        <label asp-for="Name"></label>
        <input asp-for="Name" class="form-control" />
        <span asp-validation-for="Name" class="text-danger"></span>
    </div>
    <div class="form-group">
        <label asp-for="Price"></label>
        <input asp-for="Price" class="form-control" />
        <span asp-validation-for="Price" class="text-danger"></span>
    </div>
    <div class="form-group">
        <input type="submit" value="Save" class="btn btn-primary" />
    </div>
</form>
语法详解
  • [HttpPost]:指定该方法只处理 HTTP POST 请求。
  • [ValidateAntiForgeryToken]:这是一个安全措施。它会验证表单中是否包含一个由 Html.AntiForgeryToken() 或 form 标签助手自动生成的防伪令牌,以防止跨站请求伪造(CSRF)攻击。
  • [Bind("Id,Name,Price")]:这是绑定白名单。它明确指定了哪些属性可以从请求中接收数据。这是一个重要的安全实践,可以防止 ** 过度发布(Over-Posting)** 攻击(即恶意用户尝试为你不希望他们修改的属性赋值)。
  • _context.Update(product):告诉 EF Core,这个 product 对象的属性已经被修改,需要在数据库中更新对应的记录。
  • await _context.SaveChangesAsync():将所有在 DbContext 中被跟踪的更改(包括更新)提交到数据库。
总结

处理 Edit 的 HttpPost 请求,核心流程是:

  1. 接收数据:通过模型绑定接收表单数据。
  2. 验证数据:使用 ModelState.IsValid 检查数据是否符合模型上的验证规则。
  3. 更新数据:使用 _context.Update() 标记实体为已修改。
  4. 保存数据:调用 _context.SaveChangesAsync() 将更改写入数据库。
  5. 重定向:成功后,使用 RedirectToAction 重定向到另一个页面,以防止用户刷新页面时重复提交表单。

44. 处理 404 Not Found 错误信息

大白话解释

404 Not Found 错误就是当用户访问一个不存在的 URL时,服务器返回的状态码。处理 404 错误,就是为这种情况提供一个友好的、自定义的错误页面,而不是浏览器显示的默认的、冷冰冰的错误提示。

为什么需要它
  • 提升用户体验:一个设计良好的 404 页面可以告诉用户 “页面未找到”,并提供返回首页或其他页面的链接,引导用户继续浏览。
  • 品牌形象:一个有趣的、有创意的 404 页面可以展示网站的个性。
如何处理 (针对单个 Controller)

这是一种简单但不推荐的方式,因为需要在每个 Controller 中重复编写。

csharp

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    // 捕获所有未匹配到其他Action的请求
    public IActionResult Error404()
    {
        return View(); // 返回一个自定义的404视图
    }
}

然后在 Program.cs 的路由中配置一个 “兜底” 路由。

csharp

// 不推荐的旧方式
// app.UseEndpoints(endpoints =>
// {
//     endpoints.MapControllerRoute(
//         name: "default",
//         pattern: "{controller=Home}/{action=Index}/{id?}");
//     // 兜底路由
//     endpoints.MapControllerRoute(
//         name: "404",
//         pattern: "{*url}",
//         defaults: new { controller = "Home", action = "Error404" });
// });
总结

处理单个 Controller 的 404 错误虽然可行,但不推荐,因为它不够集中和灵活。更好的方法是使用中间件进行全局统一处理,这将在下一节介绍。


45. 统一处理 404 Not Found 错误信息

大白话解释

统一处理 404 错误,就是使用ASP.NET Core 的中间件(Middleware),在应用程序的请求管道中设置一个 “关卡”。当任何地方出现 404 错误时,这个关卡都会捕获到它,并统一跳转到你指定的自定义 404 页面。

为什么需要它
  • 代码集中:只需要在一个地方(Program.cs)配置,即可对整个应用生效。
  • 易于维护:修改 404 页面时,只需修改一个视图文件。
  • 功能强大:可以处理所有类型的 404,包括静态文件不存在、API 接口不存在等。
如何处理 (使用中间件)

在 Program.cs 中添加 app.UseStatusCodePagesWithReExecute 中间件。

1. 在 Program.cs 中配置

csharp

var app = builder.Build();

// ... 其他中间件,如 UseHttpsRedirection, UseStaticFiles ...

// **关键配置:在UseRouting之前或之后添加**
// 当发生404错误时,重执行到 /Error/StatusCode/404 这个URL
app.UseStatusCodePagesWithReExecute("/Error/StatusCode/{0}");

app.UseRouting();

// ... UseAuthorization 等其他中间件 ...

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

app.Run();
  • "/Error/StatusCode/{0}" 是一个 URL 模板,{0} 会被实际的 HTTP 状态码(如 404)替换。

2. 创建 ErrorController

csharp

// Controllers/ErrorController.cs
public class ErrorController : Controller
{
    [Route("Error/StatusCode/{statusCode}")]
    public IActionResult StatusCode(int statusCode)
    {
        // 根据状态码决定返回哪个视图
        if (statusCode == 404)
        {
            return View("NotFound");
        }
        
        // 可以处理其他状态码,如 403, 500 等
        return View("Error", statusCode);
    }
}

3. 创建自定义视图在 Views/Error/ 文件夹下创建 NotFound.cshtml

html

预览

<!-- Views/Error/NotFound.cshtml -->
@{
    ViewData["Title"] = "Page Not Found";
}

<h1 class="text-danger">404 - Page Not Found</h1>
<p>Sorry, the page you are looking for does not exist.</p>
<p>@Html.ActionLink("Go back to Home Page", "Index", "Home")</p>
总结

使用 app.UseStatusCodePagesWithReExecute 中间件是统一处理 HTTP 状态码错误(包括 404)的最佳实践。它通过 “重执行” 机制,在保持原始 HTTP 状态码的同时,渲染一个自定义的视图,实现了优雅的错误处理。


46. UseStatusCodePagesWithRedirects 与 UseStatusCodePagesWithReExecute 对比

大白话解释

这两个都是处理 HTTP 状态码错误的中间件,但它们的工作方式完全不同。

  • UseStatusCodePagesWithRedirects:当发生错误时,它会告诉浏览器:“你要找的页面不在这儿,请到 /Error/404 这个新地址去看看。” 浏览器会收到一个 302 重定向指令,然后重新发起一个全新的请求
  • UseStatusCodePagesWithReExecute:当发生错误时,它会在服务器内部“悄悄” 地执行 /Error/404 这个 URL 对应的逻辑,然后把生成的页面内容直接返回给浏览器。对浏览器来说,它只发起了一次请求,收到的状态码仍然是 404。
为什么需要区分它们
  • URL 地址Redirects 会改变浏览器地址栏的 URL;ReExecute 不会。
  • 搜索引擎优化 (SEO)ReExecute 更好,因为它向搜索引擎正确地报告了 404 状态码;Redirects 会报告 302 然后是 200,可能导致搜索引擎误判。
  • 用户体验ReExecute 体验更流畅,因为没有页面跳转的感觉。
代码对比

csharp

// Program.cs

// 使用 Redirects
// 浏览器地址栏会从 /non-existent 变为 /Error/404
// 浏览器最终收到的是 200 OK 状态码
app.UseStatusCodePagesWithRedirects("/Error/StatusCode/{0}");

// 使用 ReExecute (推荐)
// 浏览器地址栏保持为 /non-existent
// 浏览器最终收到的是 404 Not Found 状态码
app.UseStatusCodePagesWithReExecute("/Error/StatusCode/{0}");
总结
特性UseStatusCodePagesWithRedirectsUseStatusCodePagesWithReExecute
工作方式服务器返回 302,浏览器重定向到新 URL服务器内部重执行新 URL 的逻辑
浏览器地址栏改变不变
最终 HTTP 状态码200 OK (来自新页面)原始状态码 (如 404, 500)
SEO 友好度较差
用户体验有跳转感无缝,体验好
推荐场景简单应用,或需要明确将用户带到错误页面的场景绝大多数 Web 应用的首选

47. 全局异常处理

大白话解释

全局异常处理,就是当你的应用程序在运行时意外崩溃(抛出一个未被捕获的异常)时,能够捕获这个错误,并向用户显示一个友好的错误页面,而不是一个让用户不知所措的 “黄屏错误页”(Yellow Screen of Death)。

为什么需要它
  • 防止应用崩溃:捕获异常可以防止程序因一个未处理的错误而完全停止服务。
  • 保护敏感信息:隐藏内部错误详情(如数据库连接字符串、堆栈跟踪),防止泄露给用户。
  • 提供友好反馈:告诉用户 “发生了未知错误”,并建议他们稍后再试或联系管理员。
如何处理 (使用 IExceptionFilter)

创建一个实现 IExceptionFilter 接口的过滤器。

1. 创建异常过滤器

csharp

// Filters/CustomExceptionFilter.cs
public class CustomExceptionFilter : IExceptionFilter
{
    private readonly ILogger<CustomExceptionFilter> _logger;
    private readonly IWebHostEnvironment _env;

    public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger, IWebHostEnvironment env)
    {
        _logger = logger;
        _env = env;
    }

    public void OnException(ExceptionContext context)
    {
        var exception = context.Exception;

        // 1. 记录异常日志 (下一节会详细讲)
        _logger.LogError(exception, "An unhandled exception occurred.");

        // 2. 准备错误信息
        var result = new ViewResult { ViewName = "Error" };
        
        // 在开发环境下,可以将异常信息传递给视图用于调试
        if (_env.IsDevelopment())
        {
            var model = new ErrorViewModel { Message = exception.Message, StackTrace = exception.StackTrace };
            result.ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), context.ModelState);
            result.ViewData.Model = model;
        }

        // 3. 将结果设置到上下文,这将阻止异常继续传播
        context.Result = result;
        
        // 4. 将异常标记为已处理
        context.ExceptionHandled = true;
    }
}

2. 在 Program.cs 中注册过滤器

csharp

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews(options =>
{
    // 将自定义异常过滤器添加到全局过滤器集合
    options.Filters.Add<CustomExceptionFilter>();
});

3. 创建错误视图 Error.cshtml

html

预览

@model MyWebApp.ViewModels.ErrorViewModel

@{
    ViewData["Title"] = "Error";
}

<h1 class="text-danger">An Error Occurred</h1>
<p>Sorry, something went wrong on our end.</p>

@if (Model != null && !string.IsNullOrEmpty(Model.Message))
{
    <div class="alert alert-danger">
        <strong>Message:</strong> @Model.Message
    </div>
}

@if (Model != null && !string.IsNullOrEmpty(Model.StackTrace) && ViewData["IsDevelopment"] != null && (bool)ViewData["IsDevelopment"])
{
    <div class="alert alert-warning">
        <strong>Stack Trace (Development Only):</strong>
        <pre>@Model.StackTrace</pre>
    </div>
}
总结

使用全局异常过滤器是处理应用程序运行时错误的标准做法。它能集中捕获所有未处理的异常,记录日志,并向用户展示一个安全、友好的错误界面。


48. 日志记录

大白话解释

日志记录就是在你的应用程序运行时,按时间顺序记录下发生的重要事件。这些事件可以是正常的操作(如 “用户登录成功”),也可以是错误(如 “数据库连接失败”)。

你可以把它想象成飞机上的 **“黑匣子”**,它忠实地记录下应用的 “一举一动”,以便在出现问题时进行排查和分析。

为什么需要它
  • 问题排查:当用户报告 bug 时,日志是你了解当时发生了什么的最主要依据。
  • 系统监控:通过分析日志,可以了解应用的运行状态、性能瓶颈和用户行为。
  • 安全审计:记录关键操作(如登录、权限变更),以便追踪和调查安全事件。
如何使用

ASP.NET Core 内置了一个非常强大的日志系统。

1. 在 Program.cs 中配置日志默认情况下,日志系统已经配置好了,会输出到控制台和调试窗口。你可以根据需要添加更多的日志提供器(如文件、数据库)。

2. 在控制器中注入并使用 ILogger

csharp

// Controllers/HomeController.cs
public class HomeController : Controller
{
    // 1. 注入ILogger<T>,T是当前类的类型,便于在日志中识别来源
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        // 2. 记录一条信息日志
        _logger.LogInformation("Home page visited.");
        return View();
    }

    public IActionResult Privacy()
    {
        try
        {
            // 模拟一个可能出错的操作
            throw new InvalidOperationException("Something went wrong in Privacy page.");
        }
        catch (Exception ex)
        {
            // 3. 记录一条错误日志,并附上异常对象
            _logger.LogError(ex, "An error occurred while processing the Privacy page.");
        }
        return View();
    }
}
日志级别

日志有不同的级别,用于区分事件的重要性:

  • Trace:最详细的日志,通常用于开发调试。
  • Debug:用于调试和开发。
  • Information:记录应用程序的正常运行情况(如用户登录、订单创建)。
  • Warning:记录不影响程序运行的异常情况(如使用了已过时的 API)。
  • Error:记录导致功能失败的错误(如数据库连接失败)。
  • Critical:记录导致应用程序崩溃或需要立即处理的致命错误(如磁盘空间耗尽)。

你可以在 appsettings.json 中配置不同日志级别的开关。

总结

日志记录是任何生产级应用都必须具备的功能。通过依赖注入 ILogger<T>,你可以在应用的任何地方轻松地记录不同级别的日志,为问题排查和系统监控提供有力支持。


49. 记录异常信息

大白话解释

记录异常信息,就是在上一节日志记录的基础上,专门针对 ** 程序运行时抛出的错误(Exception)** 进行详细记录。这包括错误的描述、发生位置(堆栈跟踪)等,以便开发者能够快速定位并修复问题。

为什么需要它

仅仅知道 “出错了” 是不够的。详细的异常日志能告诉你:

  • 错误是什么 (Exception.Message)
  • 错误发生在哪里 (Exception.StackTrace)
  • 导致错误的内部原因是什么 (Exception.InnerException)
如何使用

最常见的方式是在全局异常过滤器中记录异常。

代码示例 (延续第 47 节)

csharp

// filters/CustomExceptionFilter.cs
public void OnException(ExceptionContext context)
{
    var exception = context.Exception;

    // 核心:使用 LogError 或 LogCritical 记录异常
    // LogError方法的第二个参数是异常对象
    _logger.LogError(exception, "An unhandled exception has occurred.");

    // ... 后续处理,如返回错误视图 ...
}
  • _logger.LogError(exception, "Message"):这是记录异常的标准方式。exception 对象会被日志系统处理,自动包含其 Message 和 StackTrace 等信息。
总结

在全局异常过滤器中使用 _logger.LogError(exception, "message") 是集中、统一记录所有未处理异常的最佳实践。它确保了任何意外的程序崩溃都不会 “悄无声息” 地发生,而是被完整地记录下来,为后续的问题诊断提供了关键线索。


50. 使用 NLog 记录信息到文件中

大白话解释

NLog 是一个第三方的、功能非常强大的日志框架。使用 NLog 记录信息到文件中,就是让你的应用程序把日志按照你指定的格式和规则,自动写入到服务器上的文本文件里,而不是只在控制台或调试器里显示。

为什么需要它
  • 持久化存储:日志文件可以被长期保存,用于事后分析和审计。
  • 灵活配置:你可以自定义日志文件的名称、路径、格式、滚动策略(如每天一个文件、文件大小达到一定值后自动分割)等。
  • 性能优异:NLog 经过了高度优化,对应用程序的性能影响很小。
如何使用

1. 安装 NLog 包在你的项目中安装两个 NuGet 包。

bash

dotnet add package NLog.Web.AspNetCore
dotnet add package NLog

2. 创建 NLog 配置文件在项目根目录下创建一个名为 nlog.config 的 XML 文件。

xml

<!-- nlog.config -->
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info"
      internalLogFile="c:\temp\nlog-internal.log">

    <!-- 定义日志目标 -->
    <targets>
        <!-- 目标1: 写入文件 -->
        <target xsi:type="File" 
                name="allfile" 
                fileName="${basedir}/logs/${shortdate}.log" 
                layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception}" />
    </targets>

    <!-- 定义日志规则 -->
    <rules>
        <!-- 所有日志级别为 Info 及以上的日志,都写入到名为 "allfile" 的目标中 -->
        <logger name="*" minlevel="Info" writeTo="allfile" />
    </rules>
</nlog>
  • <targets>:定义日志要输出到哪里。这里我们定义了一个文件目标。
  • fileName="${basedir}/logs/${shortdate}.log":日志文件路径。${basedir} 是应用程序的根目录,${shortdate} 会生成 yyyy-MM-dd 格式的日期,实现每天一个日志文件。
  • layout:定义日志的格式。这里包含了时间、日志级别、日志来源和消息。
  • <rules>:定义哪些日志(logger name="*" 代表所有)在什么级别(minlevel="Info")下,输出到哪个目标(writeTo="allfile")。

3. 在 Program.cs 中集成 NLog

csharp

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 使用NLog作为日志提供器
builder.Logging.ClearProviders();
builder.Host.UseNLog();

// ... 其他服务注册 ...

var app = builder.Build();
// ...

4. 在代码中正常使用 ILogger集成完成后,你之前所有使用 ILogger<T> 的代码完全不需要任何改动。NLog 会自动接手,将日志按照 nlog.config 的配置写入到文件中。

csharp

// 在任何注入了ILogger的地方
_logger.LogInformation("This message will be written to a file by NLog.");
try
{
    throw new Exception("A test exception.");
}
catch (Exception ex)
{
    _logger.LogError(ex, "This error will also be written to the file.");
}

运行你的应用程序后,你会在项目根目录下看到一个 logs 文件夹,里面会有以日期命名的日志文件,内容如下:

plaintext

2023-10-27 15:30:00.1234|INFO|MyWebApp.Controllers.HomeController|This message will be written to a file by NLog.
2023-10-27 15:30:05.6789|ERROR|MyWebApp.Controllers.HomeController|This error will also be written to the file. System.Exception: A test exception.
   at MyWebApp.Controllers.HomeController.Index() in ...
总结

使用 NLog 将日志记录到文件中是企业级应用的标准做法。它通过简单的配置,就能让你获得一个功能强大、高度可定制的日志系统,轻松实现日志的持久化存储和管理。


这一部分我们学习了ASP.NET Core 中处理用户输入和应对运行时问题的关键技术:

  • 文件上传:包括单文件和多文件上传,是构建内容管理系统的基础。
  • Edit 操作:掌握了数据更新的完整流程和安全实践。
  • 错误处理:学会了如何优雅地处理 404 和 500 等 HTTP 错误,提升了应用的健壮性和用户体验。
  • 日志记录:理解了日志的重要性,并学会了使用内置日志系统和 NLog 框架来记录和持久化应用程序的运行信息,为问题排查和系统监控提供了有力支持。

掌握了这些,你就可以构建出功能更完善、更稳定、更易于维护的 Web 应用了。

51. LogLevel 配置及过滤日志信息

大白话解释

LogLevel(日志级别)就像给日志信息贴上 **“重要程度” 的标签 **。通过配置日志级别,你可以控制哪些级别的日志需要被记录,哪些可以被忽略。这就像你设置手机通知:只接收 “重要” 和 “紧急” 的通知,忽略 “普通” 的广告。

为什么需要它
  • 控制信息量:在开发环境,你可能想看到所有细节(DebugInformation);但在生产环境,为了性能和日志文件大小,你可能只关心错误(ErrorCritical)。
  • 聚焦问题:过滤掉低级别日志,可以让你在排查问题时更快地找到关键的错误信息。
  • 性能优化:记录大量日志会消耗系统资源。通过设置合适的日志级别,可以减少不必要的 I/O 操作,提升应用性能。
如何配置

日志级别主要在 appsettings.json 文件中配置。

appsettings.json 示例:

json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information", // 1. 默认日志级别为 Information
      "Microsoft": "Warning",   // 2. 来自 Microsoft 命名空间的日志,级别为 Warning
      "MyWebApp.Controllers": "Debug" // 3. 来自我们自己项目控制器的日志,级别为 Debug
    }
  }
}
语法详解
  • "Logging": { "LogLevel": { ... } }:这是日志配置的根节点。
  • "Default": "Information":这是全局默认配置。如果某个日志来源没有被单独配置,就会使用这个级别。Information 级别意味着 InformationWarningErrorCritical 级别的日志都会被记录。Trace 和 Debug 级别的日志会被忽略。
  • "Microsoft": "Warning":这是按命名空间过滤。所有来自 Microsoft.* 命名空间的日志(如ASP.NET Core 框架自身的日志),只有级别大于等于 Warning 的才会被记录。这可以有效地过滤掉框架输出的大量调试信息。
  • "MyWebApp.Controllers": "Debug":这是更具体的命名空间过滤。我们自己项目中 Controllers 文件夹下的日志,级别大于等于 Debug 的都会被记录。这个配置比 Default 更具体,所以它会覆盖 Default 的设置。
代码示例

在控制器中记录不同级别的日志:

csharp

// Controllers/HomeController.cs
public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        _logger.LogTrace("这是一条 Trace 日志..."); // 级别最低,默认配置下不会被记录
        _logger.LogDebug("这是一条 Debug 日志..."); // 在 "MyWebApp.Controllers" 配置下会被记录
        _logger.LogInformation("用户访问了首页。");   // 在 "MyWebApp.Controllers" 配置下会被记录
        _logger.LogWarning("这是一条警告日志!");      // 在 "MyWebApp.Controllers" 配置下会被记录
        return View();
    }
}

根据上面的 appsettings.json 配置,访问首页后,你会在日志输出中看到 DebugInformation 和 Warning 级别的日志,但看不到 Trace 级别的日志。

总结

通过在 appsettings.json 中配置 LogLevel,你可以精细地控制日志的输出粒度,实现按需记录,既能满足开发调试的需求,又能保证生产环境的性能和日志清晰。


52. 新增的路由插件 EndPoint

大白话解释

Endpoint(终结点)是ASP.NET Core 中处理路由的新方式。你可以把它想象成一张 **“精确的地图”。在旧的路由系统中,你定义一个通用模板(如 {controller}/{action}),系统在运行时动态解析。而在 Endpoint 路由系统中,应用启动时就会提前生成一张包含所有可用 URL(终结点)的 “地图”**。当请求到来时,系统直接在这张 “地图” 上查找,速度更快。

为什么需要它
  • 性能提升:启动时预计算路由,请求到来时直接匹配,减少了运行时的计算开销。
  • 功能更强大:它将路由匹配和请求处理分离开来,使得在匹配到路由后、执行处理逻辑前,可以进行更多操作(如授权、CORS 策略检查等)。
  • 更灵活:可以轻松地为不同的终结点配置不同的元数据(如授权策略)。
如何使用

在 Program.cs 中,Endpoint 路由通过 app.UseRouting() 和 app.UseEndpoints(...) 两个中间件来实现。

Program.cs 示例:

csharp

var app = builder.Build();

// ... 其他中间件 ...

// 1. 启用路由匹配
// 这个中间件负责根据请求的URL,在预先生成的“地图”上找到对应的 Endpoint
app.UseRouting();

// 2. 可以在这里放置需要在路由匹配后、执行前运行的中间件,如授权
app.UseAuthorization();

// 3. 启用终结点执行
// 这个中间件负责执行找到的 Endpoint 对应的处理逻辑(如Controller的Action方法)
app.UseEndpoints(endpoints =>
{
    // 配置MVC控制器的路由
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    // 配置一个简单的“ Hello World ”终结点
    endpoints.MapGet("/hello", async context =>
    {
        await context.Response.WriteAsync("Hello from the endpoint!");
    });

    // 配置一个API控制器的路由 (如果是Web API项目)
    // endpoints.MapControllers();
});

app.Run();
语法详解
  • app.UseRouting():这是路由系统的第一阶段。它的任务是接收 HTTP 请求,并根据 URL 找到对应的 Endpoint
  • app.UseEndpoints(...):这是路由系统的第二阶段。它的任务是执行 UseRouting() 找到的那个 Endpoint
  • endpoints.MapControllerRoute(...):这是为 MVC 控制器注册传统路由的方法。
  • endpoints.MapGet("/hello", ...):这是直接定义一个终结点的简洁方式。它将 GET /hello 这个 URL 直接映射到一个 lambda 表达式,无需创建 Controller。这对于创建简单的 API 或健康检查接口非常方便。
总结

Endpoint 路由是ASP.NET Core 现代架构的核心部分。它通过预计算路由表的方式提升了性能,并提供了更强大的扩展性,是所有新ASP.NET Core 项目的默认路由方式。


53. 授权与验证中间件

大白话解释

这两个中间件是保护你网站安全的 **“门卫” 和 “安检员”**。

  • 验证(Authentication)中间件 (UseAuthentication):这个是 **“门卫”**。它的工作是检查用户的 “身份证”(如 Cookie、JWT 令牌),确认你是谁。如果验证通过,它会在系统中创建一个 “已登录用户” 的凭证。
  • 授权(Authorization)中间件 (UseAuthorization):这个是 **“安检员”**。它的工作是在你试图进入某个 “ restricted area ”(受保护的页面或 API)时,检查你的 “通行证”(角色、权限),看你是否有资格进入。

执行顺序非常重要:必须先验证身份,再检查授权。

为什么需要它们
  • 保护敏感信息:确保只有登录用户才能访问个人信息、后台管理等页面。
  • 实现权限控制:不同用户有不同权限。例如,普通用户只能查看,管理员才能删除数据。
如何使用

1. 在 Program.cs 中配置和启用

csharp

var builder = WebApplication.CreateBuilder(args);

// 1. 添加验证服务 (例如,使用Cookie验证)
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login"; // 如果未登录,自动跳转到登录页
    });

// 2. 添加授权服务
builder.Services.AddAuthorization();

// ... 添加MVC服务等 ...
builder.Services.AddControllersWithViews();

var app = builder.Build();

// ... 其他中间件 ...
app.UseStaticFiles();

// 3. 启用验证中间件 (门卫)
app.UseAuthentication();

// 4. 启用授权中间件 (安检员)
// **注意:必须在 UseRouting 之后,UseEndpoints 之前**
app.UseAuthorization();

app.UseRouting();

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

app.Run();

2. 在 Controller 或 Action 上应用授权使用 [Authorize] 特性来标记需要保护的资源。

csharp

// Controllers/AdminController.cs
[Authorize] // 整个控制器都需要登录才能访问
public class AdminController : Controller
{
    public IActionResult Dashboard()
    {
        // 只有已登录用户才能看到这个页面
        return View();
    }

    [Authorize(Roles = "SuperAdmin")] // 更严格的要求:必须是 "SuperAdmin" 角色
    public IActionResult Settings()
    {
        // 只有 "SuperAdmin" 角色的用户才能看到这个页面
        return View();
    }
}
总结

UseAuthentication 和 UseAuthorization 是构建安全 Web 应用的基石。它们分工明确:

  1. UseAuthentication“你是谁?” (验证身份)
  2. UseAuthorization“你能做什么?” (检查权限)

在 Program.cs 中,它们的注册顺序绝对不能颠倒


54. 部署与发布到 IIS 环境

大白话解释

部署与发布就是把你在电脑上开发好的ASP.NET Core 网站,搬到一台专门的服务器上,并配置好,让所有人都能通过互联网访问它。IIS (Internet Information Services) 就是 Windows 服务器上最常用的 “网站管家”,负责接收用户的请求,并把它们交给你的ASP.NET Core 应用来处理。

为什么需要它

开发完成的网站最终是要上线运行的,让用户能够访问。部署就是实现这个目标的最后一步。

如何部署

部署到 IIS 主要分为两大步:发布应用 和 在 IIS 上配置网站

第一步:发布ASP.NET Core 应用

  1. 在 Visual Studio 中,右键点击你的项目 -> 发布 (Publish)
  2. 在 “发布目标” 中,选择 文件夹 (Folder),然后指定一个本地文件夹路径(如 D:\MyWebApp-Published)。
  3. 点击 “发布” 按钮。Visual Studio 会将你的应用程序及其所有依赖项编译、打包,并复制到你指定的文件夹中。

第二步:在 IIS 上创建和配置网站

  1. 安装 IIS:确保你的 Windows 服务器已经安装了 IIS。
  2. 安装ASP.NET Core Module (ANCM):这是一个关键组件,它是 IIS 和ASP.NET Core 应用之间的 “桥梁”。你需要在服务器上安装 ASP.NET Core Hosting Bundle。它包含了运行ASP.NET Core 应用所需的 runtime 和 ANCM。
  3. 创建网站
    • 打开 “Internet Information Services (IIS) 管理器”。
    • 在左侧 “连接” 窗格中,右键点击 “网站 (Sites)” -> 添加网站 (Add Website...)
    • 网站名称:给你的网站起个名字(如 MyWebApp)。
    • 物理路径:选择你刚才发布应用的文件夹(D:\MyWebApp-Published)。
    • 端口:指定一个端口号(如 80,如果是默认网站)。
    • 点击 “确定”。
  4. 配置应用程序池 (Application Pool)
    • 在 IIS 管理器中,点击 “应用程序池 (Application Pools)”。
    • 找到你刚才创建的网站对应的应用程序池(通常和网站同名),右键点击 -> 高级设置 (Advanced Settings...)
    • 将 .NET CLR 版本 (.NET CLR Version) 设置为 “无托管代码 (No Managed Code)”。这是因为ASP.NET Core 应用是自包含的,它运行在自己的 Kestrel 服务器上,IIS 只是一个反向代理。
  5. 测试访问:在浏览器中输入 http://服务器IP地址:端口号,如果一切顺利,你就能看到你的网站了!
总结

部署ASP.NET Core 到 IIS 的核心是:

  1. 发布:将应用打包成可部署的文件。
  2. 安装宿主包:在服务器上安装 ASP.NET Core Hosting Bundle
  3. 配置 IIS:创建网站,指向发布文件夹,并将应用程序池的.NET CLR版本设置为无托管代码

55. 安装 Windows 环境部署项目到 IIS 中

大白话解释

这一节是上一节的具体操作指南。它详细说明了在一台全新的 Windows 服务器上,从安装必要的软件开始,到最终成功部署项目的每一个步骤。

为什么需要它

对于新手来说,服务器环境的配置可能比开发应用本身更复杂。这一节提供了一个从零开始的、一步一步的 “傻瓜式” 教程,确保你能成功地把应用跑起来。

详细步骤

第 1 步:准备一台 Windows 服务器你需要一台安装了 Windows Server 操作系统的电脑或虚拟机。

第 2 步:安装 IIS (Internet Information Services)

  1. 打开 “服务器管理器 (Server Manager)”。
  2. 点击 “添加角色和功能 (Add Roles and Features)”。
  3. 在 “安装类型 (Installation Type)” 中,选择 “基于角色或基于功能的安装 (Role-based or feature-based installation)”。
  4. 在 “服务器选择 (Server Selection)” 中,选择你当前的服务器。
  5. 在 “服务器角色 (Server Roles)” 中,勾选 “Web 服务器 (IIS)” (Web Server (IIS))。点击 “下一步” 时,系统会提示你添加 IIS 所需的管理工具,点击 “添加功能 (Add Features)”。
  6. 后续步骤保持默认,一直点击 “下一步”,最后点击 “安装 (Install)”。等待安装完成。

第 3 步:安装ASP.NET Core Hosting Bundle这是最关键的一步!

  1. 在服务器上打开浏览器,访问ASP.NET Core 的官方下载页面:https://dotnet.microsoft.com/download/dotnet
  2. 找到与你项目版本匹配的ASP.NET Core 版本,下载 ASP.NET Core Hosting Bundle”
  3. 下载完成后,双击安装包进行安装。这个安装包会同时安装:
    • .NET Core Runtime:让你的应用能在服务器上运行。
    • ASP.NET Core Module (ANCM):让 IIS 能把请求转发给你的ASP.NET Core 应用。
  4. 安装完成后,必须重启 IIS 或整个服务器,以使 ANCM 生效。在命令提示符中运行 iisreset 是一个快速重启 IIS 的好方法。

第 4 步:发布你的ASP.NET Core 项目在你的开发电脑上,按照第 54 节的说明,使用 Visual Studio 将项目发布到一个本地文件夹。

第 5 步:将发布文件复制到服务器将你发布好的文件夹(如 MyWebApp-Published)通过远程桌面、FTP 或共享文件夹等方式,完整地复制到服务器的一个目录下(如 C:\inetpub\wwwroot\MyWebApp)。

第 6 步:在 IIS 中创建网站

  1. 打开 “IIS 管理器”。
  2. 右键点击 “网站” -> “添加网站”。
  3. 网站名称MyWebApp
  4. 物理路径C:\inetpub\wwwroot\MyWebApp (选择你刚刚复制过来的文件夹)
  5. 端口80 (或其他未被占用的端口)
  6. 点击 “确定”。

第 7 步:配置应用程序池

  1. 在 IIS 管理器中,点击 “应用程序池”。
  2. 找到名为 MyWebApp 的应用程序池,右键点击 -> “高级设置”。
  3. 将 .NET CLR 版本 设置为 “无托管代码”
  4. 将 托管管道模式 (Managed Pipeline Mode) 设置为 “集成” (Integrated)
  5. 点击 “确定”。

第 8 步:测试访问在任何一台能访问服务器的电脑上,打开浏览器,输入 http://服务器的IP地址 (如果端口是 80) 或 http://服务器的IP地址:端口号。如果看到你的网站首页,恭喜你,部署成功!

总结

ASP.NET Core 项目部署到全新的 Windows 服务器上,遵循这个 **“安装 IIS -> 安装 Hosting Bundle -> 发布项目 -> 配置 IIS”** 的流程,就能确保你的应用顺利上线。其中,安装 Hosting Bundle 并重启 IIS是最容易被忽略但至关重要的一步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值