C#笔记——使用ASP.NET和Entity Framework Core

本文最后更新于:2023年11月4日 下午

环境配置

首先检查下自己电脑装没装 .NET,建议用 vs 当懒狗,直接全装了。在终端中输入命令:dotnet --list-sdks,如果没列出任何版本或者未找到命令就是没装,赶紧给我装啊!.jpg
至于项目文件,全都是文档中有提到的,看看文档就找到了!

项目基本结构

属于十分经典的 API 项目结构啊:

  • Models: 保存实体模型,如 POCO 或者说 DTO。
  • Services: 业务逻辑层。
  • Controllers: 控制器层,负责搜集参数、参数校验、调用 Service(服务)等。
  • Data: 保存数据库相关,如DbContext

使用 ASP.NET Core 控制器创建 Web API

创建 Web API 项目

Visual Studio

新建项目中搜索: ASP.NET Core Web API ,然后创建即可。

Visual Studio Code

在终端窗口中进入对应文件夹,运行命令:dotnet new webapi -f net7.0。该命令根据当前文件夹名称命名 C# 项目文件。

接口测试工具

我们可以用 Postman 来测试接口,也可以用 .NET HTTP REPL 工具来测试接口。前者直接官网下载即可,后者则在终端运行该命令:dotnet tool install -g Microsoft.dotnet-httprepl

.NET HTTP REPL 食用方法

通过以下命令连接:httprepl https://localhost:{port};或者,在Httprepl运行时随时运行以下命令:connect https://localhost:{port}

如果Httprepl工具发出“找不到 OpenAPI 说明”的警告,最有可能的原因是开发证书不受信任。执行命令:dotnet dev-certs https --trust配置系统以信任开发证书。

浏览可用的 endpoint:ls
进入某一个 endpoint:cd endpointName
发出请求:getpostputpatchdelete,若带参数,加在后面即可,如:get 5。需要 body,那就用-c然后加在后边,如:post -c "{"id": 3, "name": "MyTest", "isFree": false}"put 3 -c "{"id": 3, "name": "MyTest", "isFree": false}"
结束当前Httprepl会话:exit

创建 ASP.NET Core Web API 控制器

控制器应该放在 Controllers 文件夹下,操作通过路由被公开为 HTTP endpoint。命名格式为:XxxController,其实就是加一个Controller后缀。我们的https://localhost:{port}/xxx的各种 HTTP 请求将执行XxxController类下的对应方法。

注意Web API的控制器的基类应该是ControllerBase而不是Controller,后者派生自前者并且添加了对视图的支持,用来处理网页而不是Web API请求。

我们还需要将两个重要属性应用到我们创建的控制器上,这两个属性分别是:[ApiController][Route("[controller]")]
[ApiController]启用固定行为,使生成 Web API 更加容易。如果要在多个控制器上应用该属性,一种方法是创建通过[ApiController]属性批注的自定义基控制器类,比如:

1
2
3
4
5
6
7
8
[ApiController]
public class MyControllerBase : ControllerBase {}

[Route("[controller]")
public class MyController : MyControllerBase
{
...
}

又或者是将该属性应用到程序集,如修改Program.cs

1
2
3
4
using Microsoft.AspNetCore.Mvc;
[assembly: ApiController]
var builder = WebApplication.CreateBuilder(args);
...

不过需要的是,如果某个控制器应用了[ApiController]属性,那么同时也要对该控制器应用[Route]属性,该属性说明见下面。
[Route("[controller]")]属性定义路由模式。[controller]令牌(参数)替换为控制器的名称(不区分大小写,无 Controller 后缀)。举两个例子:

  1. [Route("[controller]")]应用到AnimalController类上,则该控制器处理对https://localhost:{port}/animal的请求。
  2. [Route("api/[controller]")]应用到AnimalController类上,则该控制器处理对https://localhost:{port}/api/animal的请求。

在控制器中实现 REST 谓词

Get

不带参数的实现:

1
2
3
4
5
[HttpGet]
public ActionResult<TEntity> GetMethodName()
{
...
}

带参数的实现(注意int只是举个例子,实际是什么类型就填什么):

1
2
3
4
5
[HttpGet("{id}")]
public ActionResult<TEntity> GetMethodName(int id)
{
...
}

POST

将项作为参数传递入方法时,ASP.NET Core 会自动发送到 endpoint 的任何应用程序/JSON 转换为填充的 .NET TEntity 对象。

1
2
3
4
5
[HttpPost]
public IActionResult CreateMethodName(TEntity entity)
{
// This code will save the entity and return a result
}

PUT

1
2
3
4
5
[HttpPut("{id}")]
public IActionResult Update(int id, TEntity entity)
{
// This code will update the entity and return a result
}

PATCH

基本包貌似不提供这个词操作,详情见:ASP.NET Core Web API 中的 JSON 修补程序

DELETE

1
2
3
4
5
[HttpDelete("{id}")]
public IActionResult DeleteMethodName(int id)
{
// This code will delete the entity and return a result
}

Swashbuckle 和 ASP.NET Core 入门

如何为我的 Web API 应用添加 Swashbuckle

包安装

Visual Studio

用 NuGet 可以快速管理当前项目的各个包,将“包源”设置为“nuget.org”,然后直接在搜索框中搜索“Swashbuckle.AspNetCore”,安装最新的“Swashbuckle.AspNetCore”包即可。下面的步骤根据 Visual Studio 2022 编写:

  1. “工具” -> “NuGet 包管理器” -> “管理解决方案的 NuGet 包”,然后会打开一个窗口。
  2. 选择浏览,然后直接在搜索框中搜索“Swashbuckle.AspNetCore”,安装最新的“Swashbuckle.AspNetCore”包即可。

Visual Studio Code

从“终端”窗口中运行命令(注意在项目文件夹下):dotnet add Xxx.csproj package Swashbuckle.AspNetCore -v 6.5.0。命令中的Xxx换成自己的项目文件名。根据自己的需要选择版本号,编写该文章时候的最新稳定版为6.5.0

添加并配置 Swagger 中间件

添加以下代码到Program.cs中:

1
2
3
4
5
6
7
builder.Services.AddSwaggerGen();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

注意到app.Environment.IsDevelopment(),这句代码表明:仅当将当前环境设置为“开发”时,才会添加 Swagger 中间件。

如果使用目录及 IIS 或反向代理,请使用./前缀将 Swagger 终结点设置为相对路径。例如./swagger/v1/swagger.json

自定义和扩展

API 信息和说明

传递给AddSwaggerGen方法的配置操作会添加诸如作者、许可证和说明的信息。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Description = "Pizza API",
TermsOfService = new Uri("https://example.com/terms"),
Contact = new OpenApiContact
{
Name = "MaP1e-G's Blog",
Url = new Uri("https://map1e-g.github.io/about")
},
License = new OpenApiLicense
{
Name = "Example License",
Url = new Uri("https://example.com/license")
}
});
});

启动来看看效果:Swagger Information

XML 注释

要启用 XML 注释功能,就将GenerateDocumentationFile添加到.csproj(C#项目)文件:

1
2
3
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

一些 Swagger 功能无需使用 XML文档文件即可起作用,但是对于大多数功能(即方法摘要以及参数说明和响应代码说明),必须使用 XML 文件。所以可以添加以下代码,通过反射生成与 Web API 项目相匹配的 XML 文件名:

1
2
3
4
5
6
7
8
9
builder.Services.AddSwaggerGen(options =>
{
// other options
...
// using System.Reflection
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
}
);

AppContext.BaseDirectory属性用于构造 XML 文件的路径。

元素

启用 XML 注释后,三斜杠注释将被添加到 Swagger UI 中,比如:

1
2
3
4
5
6
7
8
9
/// <summary>
/// 查找所有Pizza
/// </summary>
/// <returns></returns>
[HttpGet]
public IEnumerable<Pizza> GetAll()
{
return _service.GetAll();
}

看看效果:Swagger XML <summary>

元素

还可以添加<remarks>元素来提供额外说明,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/// <summary>
/// 创建一个新Pizza
/// </summary>
/// <param name="newPizza"></param>
/// <returns></returns>
/// <remarks>
/// 示例:
///
/// POST /Pizza
/// {
/// "id": 1,
/// "name": "XxxPizza",
/// "sauce": {
/// "id": 1,
/// "name": "XxxSauce",
/// "isVegan": false
/// },
/// "topping": [
/// {
/// "id":1,
/// "name": "XxxTopping",
/// "calories": 100
/// }
/// ]
/// }
///
/// </remarks>
[HttpPost]
public IActionResult Create(Pizza newPizza)
{
var pizza = _service.Create(newPizza);
return CreatedAtAction(nameof(GetById), new { id = pizza!.Id }, pizza);
}

看看效果:Swagger XML <remarks>

元素

我们还可以在 XML 注释中表示错误代码和响应类型,比如在Create方法的 XML 注释中,加入如下注释:

1
2
/// <response code="201">Returns the newly created item</response>
/// <response code="400">If the item is null</response>

我们还需要为Craete方法添加如下的额外的属性:

1
2
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]

来看看效果:Post
同样的,我们为Get方法也添加下 XML 注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <summary>
/// 根据输入的id来查找对应的Pizza
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
/// <response code="200">返回一个美味的 Pizza</response>
[HttpGet("{id}")]
[ProducesResponseType(typeof(Pizza), StatusCodes.Status200OK)]
public ActionResult<Pizza> GetById(int id)
{
var pizza = _service.GetById(id);

if (pizza is not null)
{
return pizza;
}
else
{
return NotFound();
}
}

然后看看效果:GetById

元素和元素

就如同他们的中文意思一般,为这两个元素写 XML 注释,其实就是给对应参数和返回类型写注释。下面为Get方法额外添加一点注释:

1
2
/// <param name="id">Pizza的唯一指定ID</param>
/// <returns>返回一个美味的 Pizza</returns>

看看效果:Swagger XML <param>

使用 Entity Framework Core 持久保存和检索关系数据

什么是 EF Core?

“Entity Framework Core (EF Core) 是对象-关系映射程序 (ORM)。 ORM 在代码和数据库中实现的域模型之间提供一个层。 EF Core 是一种数据访问 API,允许使用 .NET 普通旧公共运行时语言 (CLR) 对象 (POCO) 和强类型语言集成查询 (LINQ) 语法与数据库进行交互。
在 EF Core 中,数据库可在 .NET POCO 后面抽象化。 EF Core 处理与基础数据库的直接交互。 使用此 API 时,可以花更少的时间来与数据库之间转换请求以及编写 SQL,从而让你有更多时间专注于重要的业务逻辑。”
在 EF Core 中,有一个很重要的类,必须了解和掌握,那就是:DbContext(数据库上下文)类。
DbContext是表示工作单元的特殊类。DbContext提供的方法可用于配置选项、连接字符串、日志记录以及用于将域映射到数据库的模型。
派生自DbContext的类:

  • 表示与数据库之间的活动会话
  • 保存和查询实体的实例
  • 包括DbSet<T>类型的属性,其表示数据库中的表。

包安装

要使用 EF Core 进行开发,我们依然要安装对应包。前面已经提到过 VS 可以轻松通过 Nuget 包管理器 来进行安装所以这里不提了。主要是通过命令行的安装:

  1. 首先运行:dotnet add package Microsoft.EntityFrameworkCore.Sqlite,因为项目用的是sqlite所以这里最后面是Sqlite,你也可以相应地换成MySQL或是其他数据库。
  2. 然后运行:dotnet add package Microsoft.EntityFrameworkCore.Design,该命令添加 EF Core 工具所需的包。
  3. 最后运行:dotnet tool install --global dotnet-ef,该命令将安装dotnet ef,用于创建迁移和基架的工具。如需更新,则运行dotnet tool update --global dotnet-ef

现有模型迁移至数据库

搭建模型和 DbContext 的基架

Models文件夹下搭建我们需要的模型,这些模型在迁移之后就会成为数据库中的各个表,比如例子中的PizzaToppingSauce模型,迁移后就是PizzaToppingSauce三张表。
模型搭建完毕后,我们还需要在Data文件夹下添加并配置DbContext实现。DbContext是一个网关,可以通过该网关与数据库进行交互。
在本例中,我们添加了一个继承自DbContext类的PizzaContext类,用于跟数据库进行交互。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using Microsoft.EntityFrameworkCore;
using ContosoPizza.Models;

namespace ContosoPizza.Data;

public class PizzaContext : DbContext
{
public PizzaContext (DbContextOptions<PizzaContext> options)
: base(options)
{
}

public DbSet<Pizza> Pizzas => Set<Pizza>();
public DbSet<Topping> Toppings => Set<Topping>();
public DbSet<Sauce> Sauces => Set<Sauce>();
}

可以发现,如果没其他要求的话,我们只需要将模型的DbSet集合添加至类中即可,DbSet<T>属性也对应于要在数据库中创建的表。
然后,设置数据源,在Program.cs中,添加该代码:builder.Services.AddSqlite<XxxContext>("Data Source=Xxx.db")。自己在自己的应用中实现时,将XxxContextXxx.db替换掉就好了,例子中这两处地方分别为PizzaContextContosoPizza.db

创建并运行迁移

运行该命令来创建迁移:dotnet ef migrations add MigrationName --context XxxContext
在实际使用中,将命令中的MigrationName替换为自己想要创建的迁移的名字,迁移将被命名为该名字;XxxContext则替换为对应的类的名字,如PizzaContext
运行完该命令后,项目中将会生成Migrations文件夹,在该文件夹下包含<timestamp>_MigrationName.cs文件。
创建完迁移后,还需要运行该命令:dotnet ef database update --context XxxContext,该命令将会应用迁移。

EF Core 约定通过推断开发者的意图来缩短开发时间。 例如,名为Id<entity name>Id的属性被推断为生成的表的主键。如果选择不采用命名约定,则该属性必须使用[Key]特性进行批注或配置为DbContextOnModelCreating方法中的键。

更改模型和更新数据库架构

某些时候我们可能需要对之前的模型进行修改,然后再把修改同步至数据库。要执行这样的操作,和上一节“创建并运行迁移”的操作是一样的,即先创建迁移,再应用迁移。不过请注意进行版本控制和备份以防数据丢失。

与数据交互

编写业务代码逻辑一般都是在 Service 层上,所以这里默认是编写在对应的Service类上的。
要与对应的数据库进行交互,我们就需要有对应的Context类的字段,如:private readonly PizzaContext _context,并在构造函数中将参数分配给该字段。
要访问数据库中的某张表,获取对应的DbSet属性即可,如:_context.Pizzas。这里再放几个链式调用中常见的方法:

  • AsNoTracking扩展方法指示 EF Core 禁用更改跟踪。如果某个操作是只读的,该方法可以优化性能。
  • ToList,转换成列表,懂得都懂。
  • Include扩展方法采用 lambda 表达式来指定将某些导航属性(在例子中是ToppingSauce,这两个都是 模型/数据库中的表,而且包含在Pizza模型中)通过使用预先加载来包含在结果中,如果不使用此表达式, EF Core 会为这些属性返回null。(我觉得可以理解为如果是引用属性那就需要用这个表达式来将其包含在结果集中)
  • SingleOrDefault方法返回与 lambda 表达式匹配的结果。如果没有匹配的结果,则返回null;如果有多个匹配的结果,将会引发异常。
  • Find是按主键查询记录的优化方法。
  • Remove方法根据传入的对象来移除 EF Core 对象图中的对应实体。
    在执行修改操作后,我们还需要执行_context.SaveChanges()来将操作结果应用到数据库。

现有数据库逆向工程至模型

如果我们已经有了数据库,想要根据现有数据库来生成基架代码,有没有什么方便又减少工作量的办法呢?
答案是有的。运行命令:dotnet ef dbcontext scaffold "Data Source=Xxx/Xxx.db" Microsoft.EntityFrameworkCore.Sqlite --context-dir Data --output-dir Models。该命令将使用提供的连接字符串("Data Source=Xxx.db")搭建DbContext类和模型类基架;指定使用Microsoft.EntityFrameworkCore.Sqlite数据库提供程序;--context-dir指定DbContext类的目录,--output-dir指定模型(Model)类的目录。
创建完后别忘了检查一遍,如果有需要也可以对DbContext和模型类进行修改。

如果数据库发生更改,可以生成新的基架文件。生成的文件每次都会被覆盖,但会创建为partial,因此你可以在自己的单独文件中使用自定义属性和行为来扩展它们。

最后,别忘了在Program.cs中将新生成的DbContext类注册到依赖项注入系统:builder.Services.AddSqlite<XxxContext>("Data Source=Xxx.db");

ASP.NET Core Web API 中控制器操作的返回类型

IActionResult 类型

当操作中可能有多个ActionResult返回类型时,适合使用IActionResult返回类型。ActionResult类型表示多中 HTTP 状态代码。派生自ActionResult的任何非抽象类都限定为有效的返回类型,例如:BadRequestResult(400)、NotFoundResult(404)、OkObjectResult(200)。或者,可以使用ControllerBase类中的便利方法从操作返回ActionResult类型,例如:return BadRequest();,是return new BadRequestResult();的简写形式。

同步操作

1
2
3
4
5
6
7
8
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Product))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById_IActionResult(int id)
{
var product = _productContext.Products.Find(id);
return product == null ? NotFound() : Ok(product);
}

异步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateAsync_IActionResult(Product product)
{
if (product.Description.Contains("XYZ Widget"))
{
return BadRequest();
}

_productContext.Products.Add(product);
await _productContext.SaveChangesAsync();

return CreatedAtAction(nameof(GetById_IActionResult), new { id = product.Id }, product);
}

ActionResult 类型

ActionResult<T>支持返回从ActionResult派生的类型或返回特定类型。

同步操作

1
2
3
4
5
6
7
8
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById_ActionResultOfT(int id)
{
var product = _productContext.Products.Find(id);
return product == null ? NotFound() : product;
}

异步操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[HttpPost()]
[Consumes(MediaTypeNames.Application.Json)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> CreateAsync_ActionResultOfT(Product product)
{
if (product.Description.Contains("XYZ Widget"))
{
return BadRequest();
}

_productContext.Products.Add(product);
await _productContext.SaveChangesAsync();

return CreatedAtAction(nameof(GetById_ActionResultOfT), new { id = product.Id }, product);
}

ActionResult 与 IActionResult

对比一下上面几部分代码,就可以发现,IActionResult返回的是Ok(Entity),把实体类放入OkObjectResult类中;而ActionResult<T>则是直接返回实体类。


这里有一只爱丽丝

希望本文章能够帮到您~


C#笔记——使用ASP.NET和Entity Framework Core
https://map1e-g.github.io/2023/10/19/CSharp-learning-9/
作者
MaP1e-G
发布于
2023年10月19日
许可协议