ASP.NET Core学习笔记-3:Web API 基础、基础组件

本文最后更新于:2026年1月27日 下午

第一个 Web API 项目!

项目结构

项目结构

Controller

Web API 中的控制器类都继承自 ControllerBase 类,并为其添加 [ApiController] 这个 Attribute。示例代码中的另外一个 [Route("[controller]")],用于设定路由规则,其中的 [controller] 代表控制器的名字,也就是等于 WeatherForecast

Restful

“幂等”操作:对于一个接口采用同样的参数请求一次和请求多次的结果是一致的,不会因多次请求而产生副作用。例如:发表评论成功,服务器返回结果给客户端,但是由于网络波动,客户端没能收到“发表成功”的返回结果,此时再次进行发表评论的操作,如果最终只发表了一次评论,那么这个操作就是幂等的;否则就不是。

Swagger

可以理解成提供了对应接口的文档的内置的 Postman(提供接口测试)。
注意:如果控制器中存在一个没有添加 [HttpGet][HttpPost]Attributepublic 方法,Swagger 就会报错“Failed to load API definition”。解决办法:对应方法添加:[ApiExplorerSettings(IgnoreApi = true)]``Attribute

ActionResult

ActionResult<T> 通过隐式转换重载来完成 BadRequestNotFound 等方法返回的非泛型的 ActionResultActionResult<T> 的转换,它也会完成具体值(比如某一实体类)到 ActionResult<T> 的隐式转换。因此在操作方法中,我们可以直接:return new Book()return NotFound() 等。

操作方法的参数来源

URL

[HttpGet][HttpPost]Attribute 中使用占位符 {ParameterName} 来捕捉路径中的内容:

1
2
[HttpGet("bookTitle/{title}/bookAuthor/{author}")]
public ActionResult<Book> GetBookInfo(string title, [FromRoute(Name = "author")]string mainAuthor);

QueryString

对于使用 QueryString 传递的参数,则是使用 [FromQuery] 来获取值:

1
2
// 每页五条数据,获取第十页数据的 QueryString: pNum=10&pageSize=5
public ActionResult<Book[]> GetBooks([FromQuery(Name = "pNum")]string pageNum, [FromQuery]string pageSize);

请求报文体

注意 Content-Type,在 Web API 开发中一般都是 application/json

1
2
[HttpPost]
public ActionResult AddNewPerson(Person person);

允许跨域

在 Program.cs 的 var app = builder.Build(); 前添加:

1
2
string[] urls = new[] { "http://localhost:5173" };
builder.Services.AddCors(options => options.AddDefaultPolicy(builder => builder.WithOrigins(urls).AllowAnyMethod().AllowAnyHeader().AllowCredentials()));

再在 app.UseHttpsRedirection(); 前添加:app.UseCors();

ASP.NET Core 基础组件

ASP.NET Core 中的依赖注入

在 Program.cs 中,WebApplication.CreateBuilder(args) 所返回的对象类型为 WebApplicationBuilder,它的 Services 属性的类型则是 IServiceCollection,所以也拥有 AddScoped() 等方法。一般把服务注册到这里就行。

食用指北

首先,创建一个服务类MyExampleService,并随便实现一个方法:

1
2
3
4
5
6
7
public class MyExampleService
{
public IEnumerable<string> GetNames()
{
return new string[] { "Alice", "Joe", "Maple" };
}
}

然后,在 Program.cs 中注册服务:builder.Services.AddScoped<MyExampleService>();
最后,就可以在需要用到该服务的控制器的构造函数(Constructor)中注入服务啦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Route("api/[controller]/[action]")]
[ApiController]
public class PersonsController : ControllerBase
{
private readonly MyExampleService exampleService;
public PersonsController(MyExampleService exampleService)
{
this.exampleService = exampleService;
}

[HttpGet]
public string Test()
{
var names = exampleService.GetNames();
return string.Join(", ", names);
}
}

但是有时候可能遇到特殊情况:如果一个操作方法(比如上面的 Test() 就是一个操作方法)用到的服务(在上面的例子中,Test() 方法需要用到 MyExampleService 服务)的注入比较消耗资源,而这个操作方法被调用到的频率又很低,那么在调用这个控制器中的其他方法时,又会重复注入这个服务,就很浪费资源。
为了避免这种情况,我们可以把调用方法所用到的服务,通过调用方法的参数来注入:

1
2
3
4
5
6
[HttpGet]
public string Test([FromServices]exampleService)
{
var names = exampleService.GetNames();
return string.Join(", ", names);
}

将配置系统集成到 ASP.NET Core 中

多环境设置

为了确定运行时环境,ASP.NET Core 会从环境变量中读取名字为 ASPNETCORE_ENVIRONMENT 的值,这个值就是程序运行环境的名字。推荐采用如下3个值来:Development(开发环境)、Staging(测试环境)、Production(生产环境)。如果没有设置,则认为程序运行在生产环境。
我们可以分别为各个环境创建配置文件:appsettings.Development.jsonappsettings.Staging.jsonappsettings.Production.json

用“用户机密”来避免信息泄露

对应项目点击右键,选择“管理用户机密”,Visual Studio 会自动打开 secrets.json 文件,在这个文件里就可以进行机密信息的配置了。

EF Core 与 ASP.NET Core 的集成

我们可能会将 EF Core 的项目与 ASP.NET 项目分开开发,以达到项目职责的清晰划分的目的,这个时候,我们如果要在 ASP.NET 用上下文,就需要用依赖注入的方式来配置数据库连接了,并添加对 EF Core 项目的依赖。
为了能在 ASP.NET 项目运行的时候再确定上下文对象需要连接到的数据库,我们需要修改一下上下文类:

1
2
3
4
5
6
7
8
9
10
public class MyDbContext : DbContext
{
public DbSet<Book> Books { get; set; }
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}

可以看到现在的上下文类不再重写 OnConfiguring 方法来设置数据库了,而是增加了一个 MyDbContext(DbContextOptions<MyDbContext>) 类型参数的构造方法。DbContextOptions 是一个数据库连接配置对象,所以接下来,我们将在 ASP.NET 项目中(通过依赖注入的方法)提供对 DbContextOptions 的配置:

1
2
3
4
5
builder.Services.AddDbContext<MyDbContext>(opt =>
{
string connStr = builder.Configuration.GetConnectionString("Default");
opt.UseSqlServer(connStr);
});

别忘了在 application.json 中配置连接字符串:

1
2
3
4
5
{
"ConnectionStrings": {
"Default": "Server=.;Database=demo2;Trusted_Connection=True;TrustServerCertificate=True;"
}
}

如果我们需要进行迁移,就需要为 EF Core 项目额外配置数据库连接。但是由于在 EF Core 项目中很难使用IConfiguration来读取配置,所以我们可以通过添加系统环境变量来配置数据库连接字串,并在项目中添加一个实现 IDesignTimeDbContextFactory 接口的实现类(数据库迁移工具会调用这个实现类的 CreateDbContext 方法来获取上下文对象,并使用这个对象来连接数据库)来帮助进行迁移:

1
2
3
4
5
6
7
8
9
10
public class MyDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
public MyDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<MyDbContext> builder = new();
string connStr = Environment.GetEnvironmentVariable("ConnectString:BookEFCore");
builder.UseSqlServer(connStr);
return new MyDbContext(builder.Options);
}
}

小上下文策略

“不要把项目中所有的实体类都放到同一个上下文类中,而是只把关系紧密的实体类放到同一个上下文类中…也就是项目中存在多个上下文类。”
批量注册上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static IServiceCollection AddAllDbContexts(this IServiceCollection services, Action<DbContextOptionsBuilder> builder, IEnumerable<Assembly> assemblies)
{
Type[] types = new Type[]
{
typeof(IServiceCollection),
typeof(Action<IServiceCollection>),
typeof(ServiceLifetime),
typeof(ServiceLifetime)
};
var methodAddDbContext = typeof(EntityFrameworkServiceCollectionExtensions).GetMethod("AddDbContext", 1, types);
foreach (var asmToLoad in assemblies)
{
foreach (var dbCtxType in asmToLoad.GetTypes().Where(t => !t.IsAbstract && typeof(DbContext).IsAssignableFrom(t)))
{
var methodGenericAddDbContext = methodAddDbContext.MakeGenericMethod(dbCtxType);
methodGenericAddDbContext.Invoke(null, new object[] { services, builder, ServiceLifetime.Scoped, ServiceLifetime.Scoped });
}
}
return services;
}

缓存

ASP.NET 不仅提供了把 Web 服务器的内存用作缓存的内存缓存(in-memory cache),还提供了把 Redis、数据库等用作缓存的分布式缓存(distributed cache)。

客户端响应缓存

客户端响应缓存的作用是:当同一个用户在客户端缓存存在时,发起相同的请求,那么请求不会发送至服务端,而是直接返回缓存中的内容。

How to use?

cache-control 响应报文头能够控制浏览器对请求返回内容的缓存时间等,在 ASP.NET 中,我们可以手动控制这个响应报文头,如添加 ResponseCacheAttribute

1
2
3
4
5
[HttpGet]
[ResponseCache(Duration=60)]
public DateTime Now(){
return DateTime.Now;
}

这样浏览器就会把相应内容缓存60s。

如果客户端不支持客户端缓存,那么这个设置不会生效。

服务器端响应缓存

如果在 ASP.NET Core 中安装了“响应缓存中间件”(response cache middleware),那么设置[ResponseCache]不仅会设置客户端缓存,还会设置服务端缓存。
服务器端响应缓存的作用是:当不同的用户在缓存存在期间,对服务器发起相同的请求,服务器在接收后,会优先返回缓存中的内容,而不是去调用方法。

不建议使用“响应缓存中间件”来设置服务器端响应缓存,建议采用 ASP.NET Core 提供的内存缓存、分布式缓存等机制来进行缓存。

How to use?

像客户端响应缓存那样,我们需要先给方法添加 ResponseCacheAttribute (这个步骤参考“客户端响应缓存”小节),然后再在 Program.csapp.MapControllers 之前加上 app.UseResponseCaching 即可。

如果项目启用了 CORS,请确保 app.UseCors 写在 app.UseResponseCaching 之前。

有可能遇到的问题

  1. Q: 为什么我启用了响应缓存中间件,并且禁用了浏览器的缓存,但是服务器端响应缓存却没有生效,每次发送请求都会去调用对应方法呢? A: 检查浏览器发送的请求的报文头是否包含了“cache-control: no-cache”

内存缓存

ASP.NET Core 提供了允许开发人员手动进行缓存管理的“内存缓存”机制,它将数据保存在当前运行的网站程序的内存中(可以理解成在“后端”的内存中),与进程相关。

How to use

对于 ASP.NET Core MVC 项目,框架会自动注入内存缓存 服务;对于 ASP.NET Core Web API,则需要在 Program.csbuilder.Build 之前添加 builder.Services.AddMemoryCache 来把内存缓存相关服务注册到依赖注入容器中。

IMemoryCache

我们可以通过 IMemoryCache 接口来使用内存缓存,它使用起来有点像是 IDictionary,也是通过键值对来保存内容。以下是该类的几个常用方法:

  • bool TryGetValue(object key, object value): 尝试获取键为 key 的缓存值,并保存到 value 中。成功获取返回 true,否则 false
  • void Remove(object key): 删除键为 key 的缓存值。
  • TItem Set<TItem>(object key, TItem value): 设置键为 key 的缓存值为 value
  • TItem GetOrCreate<TItem>(object key, Func<ICacheEntry, TItem> factory): 尝试获取键为 key 的缓存值,方法的返回值为获取到的缓存值。如果缓存中不存在键为 key 的缓存值,则调用 factory 来获取数据,把获取到的数据保存到缓存中,同时将其作为方法的返回值。
    通常来说,使用 GetOrCreate 来设置缓存键值对会比较好,因为其 factory 回调方法中有一个 ICacheEntry 类型的参数,这个参数能够让我们对缓存项进行一些设置,如:缓存项被清除后的回调函数、设置缓存项的优先级等。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    [HttpGet]
    public async Task<Book[]> GetBooks()
    {
    var items = await memCache.GetOrCreateAsync("AllBooks", (e) =>
    {
    return await dbCtx.Books.ToArrayAsync();
    });
    return items;
    }
    缓存在默认情况下是不会过期的,我们必须通过 ICacheEntry 来为其手动设置绝对过期时间或是滑动过期时间
  • 绝对过期时间:经过指定的时间后,清除缓存项。
  • 滑动过期时间:若指定的时间期间内该缓存项未被访问过,则清除缓存项;若访问过该缓存项,则自动续期过期时间。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public async Task<Book[]> GetBooks()
    {
    var items = await memCache.GetOrCreateAsync("AllBooks", (e) =>
    {
    e.SlidingExpiration = TimeSpan.FromSeconds(10);
    e.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
    return await dbCtx.Books.ToArrayAsync();
    });
    return items;
    }

分布式缓存

在分布式系统中,通常会有一台专门用于保存缓存的服务器,这样来自不同服务器的请求都能够通过同一个服务器来获取缓存,避免造成资源的浪费。
.NET Core 为我们提供了统一的分布式缓存服务器的操作接口,IDistributedCache,无论用什么类型的分布式缓存服务器,都能通过使用这个接口来进行缓存管理操作。
IDistributedCache 的常用方法如下:

  • Task<byte[]> GetAsync(string key): 查询键为 key 的缓存项
  • Task RefreshAsync(string key): 刷新键为 key 的缓存项
  • Task RemoveAsync(string key): 删除键为 key 的缓存项
  • Task.SetAsync(string key, byte[] value, DistributedCacheEntryOptions options): 设置键为 key 的缓存项,valuebyte[] 类型
  • Task<string> GetStringAsync(string key): 查询键为 key 的缓存项
  • Task.SetStringAsync(string key, string value, DistributedCacheEntryOptions options): 设置键为 key 的缓存项,valuestring 类型

使用 Redis 作为缓存服务器

微软官方提供了在 .NET Core 中对 Redis 的支持,只需要通过 NuGet 安装 Microsoft.Extensions.Caching.StackExchangeRedis 包即可。
要在 ASP.NET Core 应用程序中使用 Redis,请先在 Program.cs 中注册 Redis 缓存服务:

1
2
3
4
builder.Services.AddStackExchangeRedisCache(options => {
options.Configuration = "localhost"; // 不指定端口,默认为 6379
options.InstanceName = "RedisTest_"; // 注意:InstanceName会作为前缀 (Prefix) 自动加到所有通过这个 Cache 实例存储的 Key 前面。
});

接着在需要用到缓存服务的控制器中注入 IDistributedCache 服务即可,很方便。


这里有一只爱丽丝

希望本文章能够帮到您~


ASP.NET Core学习笔记-3:Web API 基础、基础组件
https://map1e-g.github.io/2024/11/02/asp-net-learning-3/
作者
MaP1e-G
发布于
2024年11月2日
许可协议