星辰大海

人生万事须自为,跬步江山即寥廓

.NET 下主要有三种方式可以进行网络请求相关操作,它们分别是 HttpWebRequest,WebClient 和 HttpClient。这三者的关系是不断递进发展的,所以一般使用 HttpClient 来进行网络请求。

本文主要讲述 HttpClient 的使用方法和进度获取。

阅读全文 »

本人使用 kobox 搭建了一个私有网盘,用 minio 搭建了一个对象存储作为 kodbox 的存储,但是最近在 kodbox 中上传稍大点的文件(大于 100K 左右)时,就提示上传失败,又很神奇的是,在 minio 却发现该文件已经成功上传了。

正文

上述问题困扰了我两天,然后偶然看到一个答案,顺利解决了。

I think the problem is caused by the proxy converting HEAD requests to GET requests.

I ran into this problem when using nginx as a reverse proxy and solved it by adding the following configuration:

proxy_cache_convert_head off;

所以,解决方法就是在 nginx 配置中添加:

1
proxy_cache_convert_head off;

后记

官方关于 minio 中反向代理的配置不全,导致怎么配置都有问题。这两天觉都没睡好,唉,坑是真的多呀,心累,不折腾了~

参考

  1. minio/minio-js#842 (comment)

本文主要介绍 minio-js 的正确安装与使用。在网上搜了好久,都没有找到一个能正常运行 minio-js 的使用教程,包括官网。所以本文对此进行总结。

阅读全文 »

本文讲述了如何在 .NET Core 的项目中从零开始搭建单元测试,然后达到项目应用的程度。通过本文,你可以 get 以下知识:

  • .NET 中现有单元测试框架有哪些
  • 为什么选择 MSTest 框架
  • 如何创建一个单元测试
  • 怎么运行单元测试

框架选型

我们在使用一种技术时,往往需要对现有技术调研,通过比较最终确定使用哪个。.NET 官方推荐的单元测试有 3 种:xUnit、NUnit、MSTest。

除了标注测试类和方法的特性用的不一样之外,它们是非常相似的。而 MSTest 与 VisualStudio 集成度更高,所以本人建议使用 MSTest。

StackOverflow 看到一条我很赞同的看法:

其实不用顾虑那么多,随便选择吧,MSTest 对 VS 的集成是最好的,而且也很容易上手,如果哪一天碰到它所无法解决的事情,切换到其他框架也非常简单,仅仅只是Nuget下个包,换下特性而已。

添加单元测试

在 VS 中,选中方法名,右键 -> 创建单元测试,点击确定。

通过上述步骤,VS 会自动创建一个单元测试项目,在该项目里面自动生成单元测试内容。

1
2
3
4
5
6
7
8
9
10
11
12
// 标记测试类
[TestClass()]
public class MinioAdapterTests
{
// 标记测试方法
[TestMethod()]
public void BucketExistsAsyncTest()
{
// 在此处编写单元测试代码
Assert.Fail();
}
}

编写测试案例

依赖注入怎么测试

ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,并且默认注入了很多服务,具体可以参考 官方文档, 相信只要使用过依赖注入框架的同学,都会对此有不同深入的理解,在此无需赘言。

然而,在引入 IOC 框架之后,对于之前常规的对于类的依赖(new Class)变成通过构造函数对于接口的依赖(ASP.NET CORE 默认注入方式),这本身更加符合依赖倒置原则,但是对于单元测试来说确会带来另一个问题:

由于层层依赖,导致在某个类的方法进行测试的时候,需要构造一大堆该类依赖的接口的实现,非常麻烦。

这个时候,我们脑子里会下意识想一个问题:为什么常用的 .Net 单元测试框架不支持依赖注入?

于是笔者带着这个问题在查阅了一些关于在单元测试中支持依赖注入的讨论Github Issue,以及其他的相关文档,突然明白一个之前一直忽视但实际却非常重要的问题:

在对于一个方法的单元测试中,我们应该关注的是这个方法内部的逻辑测试,而这个方法内部对于外部的依赖,则不在这个单元测试关注的范围内

换言之,单元测试永远都只关注需要测试的方法内部的逻辑实现,至于外部依赖方法的测试,则应该放在另一个专门针对这个方法的单元测试用例中。

弄清楚这个问题,我们才能更加理解另一个单元测试不可缺少的框架——Mock框架。在我们写的测试中,应该忽略外部依赖具体的实现,而是通过模拟该接口方法来显示的指定返回值,从而降低该返回值对于当前单元测试结果的影响,而 Mock 框架(例如最常用的Moq),刚好可以满足我们对于接口的模拟需求。

相信有同学跟我有同样的疑惑,并且当我尝试在 ASP.NET Core 单元测试中的一切外部依赖通过 Mock 的方式进行编写的时候,遇到了一些问题,下文会将这些问题一一道来,希望对有同样疑惑的同学有所帮助。

Mock 框架选择

在 .NET 中有几种 mock 框架可供选择,比如 NMock、PhinoMocks、FakeItEasy和Moq。尽管Moq相对较新,但是它非常易用。不需要像传统的 Record/Replay。并且使用 Moq 在 VS 中可以得到智能提示。学习成本也不高。

所以选择 Moq 作为 Mock 数据框架。Moq 有一个自动 Mock 库 Moq.AutoMock,建议安装该库。

Moq 基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var mock = new Mock<ILoveThisLibrary>();

// WOW! No record/replay weirdness?! :)
// 给 DownloadExists 传递一个参数,并使其返回 true
mock.Setup(library => library.DownloadExists("2.0.0.0"))
.Returns(true);

// Use the Object property on the mock to get a reference to the object
// implementing ILoveThisLibrary, and then exercise it by calling
// methods on it
ILoveThisLibrary lovable = mock.Object;
bool download = lovable.DownloadExists("2.0.0.0");

// Verify that the given method was indeed called with the expected value at most once
mock.Verify(library => library.DownloadExists("2.0.0.0"), Times.AtMostOnce());

上面的方式可以简化成:

1
2
3
4
5
6
7
8
9
10
11
12
ILoveThisLibrary lovable = Mock.Of<ILoveThisLibrary>(l =>
l.DownloadExists("2.0.0.0") == true);

// Exercise the instance returned by Mock.Of by calling methods on it...
bool download = lovable.DownloadExists("2.0.0.0");

// Simply assert the returned state:
Assert.True(download);

// If you really want to go beyond state testing and want to
// verify the mock interaction instead...
Mock.Get(lovable).Verify(library => library.DownloadExists("2.0.0.0"));

简而言之,Mock 数据的使用步骤可总结如下:

  1. 新建一个 Mock 实例 mock
  2. 通过 mock 设置方法的返回值
  3. 通过 mock.Object 获取 Mock 的对象来传递给目标方法使用

Moq.AutoMock 使用

基本使用方法

1
2
3
4
5
6
var mocker = new AutoMocker();
var car = mocker.CreateInstance<Car>();

car.DriveTrain.ShouldNotBeNull();
car.DriveTrain.ShouldImplement<IDriveTrain>();
Mock<IDriveTrain> mock = Mock.Get(car.DriveTrain);

注入现有实例

1
2
3
4
5
6
7
var mocker = new AutoMocker();

mocker.Use<IDriveTrain>(new DriveTrain());
// OR, setup a Mock
mocker.Use<IDriveTrain>(x => x.Shaft.Length == 5);

var car = mocker.CreateInstance<Car>();

结语

CI/CD 流程中应该包含单元测试

例如在编写 Repository 层进行单元测试时,经常有同学会编写依赖于数据库数据的单元测试,这样并不利于随时随地的进行单元测试检查。

如果将该流程放在 CI/CD 中,在代码的发布过程中通过单元测试可以检查代码逻辑的正确性,同时依赖于数据库的单元测试将不会通过(通常情况下,生产环境和开发环境隔离),变相迫使开发小伙伴通过 mock 方式模拟数据库返回结果。

这个原则同样适用于不能依赖三方API编写单元测试。

CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题(亦称:"集成地狱")。

点击查看更多内容

单元测试覆盖率

通常很多开发 Leader 都会要求开发团队编写单元测试,但是很少检查单元测试的质量,即单元测试最重要的指标——单元测试代码覆盖率,如果不注重覆盖率的提升,那么很有可能会导致开发成员为了单元测试而写单元测试,预期就会与实际情况相差甚远。

保证单元测试代码覆盖率,将会大大降低代码变更带来的 Bug 率,从而节省整体开发成本。

新人问题:为何要写单元测试?

对于初次开始编写单元测试的开发人员,脑中经常会对此表示怀疑:我为什么要去验证一堆我自己写的正确的逻辑?

实际这个问题包含了区分一个一般开发人员和优秀开发人员很重要的一个条件:他是否会反向思考当前逻辑的正确性。有了这种思维,看待问题才会从多个角度入手分析,对问题的本质掌握更加全面。

不要怀疑,坚持写单元测试,因为这本身也是对反向思维的一种锻炼,以笔者的经验,只有当编写过一段时间之后,才会真正认识单元测试的魅力,并且开始非常习惯的在写一段逻辑之后,顺手写了对于它的单元测试。

即使笔者也算很早就开始写单元测试了,但直到写这篇文章,仍然不断在加深对单元测试的认识。

参考

  1. C#常用单元测试框架比较:XUnit, NUnit, 和 Visual Studio(MSTest)
  2. Testing in .NET
  3. Asp.Net Core 单元测试正确姿势
  4. C#单元测试:使用Moq框架Mock对象

ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。本文以使用者的角度,对路由的使用进行概括说明,方便知识回顾与使用。

前言

ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。它支持传统路由,也支持属性路由。如果感觉到陌生,不要着急,继续向下看,下面会一一道来。

传统路由

传统路由通常在 MVC 框架中使用。

Program.cs 配置

它在 program.cs 中定义,如下:

完整方法:

1
2
3
4
5
6
app.MapControllerRoute(
// 路由名称
name: "default",
// 模板为:controllerName/actionName/{id?}
// = 号用于设置默认值
pattern: "{controller=Home}/{action=Index}/{id?}");

简化使用:

1
app.MapDefaultControllerRoute();

说明:

上面的完整路由定义中:

  • 第一个路径段 {controller=Home} 映射到控制器名称。

    如 UserController 中的控制器名为 User。

  • 第二段 {action=Index} 映射到操作名称。

    action 就是 Controller 类中的方法名。

  • 第三段 {id?} 用于可选 id。 {id?} 中的 ? 使其成为可选。 id 用于映射到模型实体。

Controller 定义

1
2
3
4
5
6
7
8
9
10
public class HomeController : Controller
{
// 默认所有的 http 谓词 get、post 等请求都会调用该接口
public IActionResult Index() {}

// 可以通过特性来约束 http 谓词的调用
// HttpPost 标记后,只有 post 才能调用这个方法
[HttpPost]
public IActionResult Create(){}
}

多个传统路由

可以多次调用 MapControllerRoute 来设置多个传统路由,如下:

1
2
3
4
5
6
app.MapControllerRoute(name: "blog",
pattern: "blog/{*article}",
defaults: new { controller = "Blog", action = "Article" });

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

上述代码中的 blog 路由是专用的传统路由。 之所以称为专用传统路由是因为 controller 和 action 不会以参数形式出现在路由模板 "blog/{*article}" 中,它们只能具有默认值 { controller = "Blog", action = "Article" }。因此,此路由将会始终映射到操作 BlogController.Article。

传统路由顺序

  1. 按定义顺序匹配

  2. 具体的路由在可变路由之前匹配

    比如 users/demo 会在 users/{userId} 之前进行匹配

特性(Attribute)路由

Attribute 本应翻译成属性,但为了与 .NET 中的属性字段区分,本文称之为特性。

特性路由通常在 REST API 中使用。

Program.cs 配置

1
2
3
4
5
6
7
8
9
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();

app.MapControllers();

app.Run();

属性路由通过调用 MapControllers 来映射属性路由控制器。

Controller 定义

下面的示例中,HomeController 匹配一组类似于默认传统路由 {controller=Home}/{action=Index}/{id?} 匹配的 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HomeController : Controller
{
[Route("")]
[Route("Home")]
[Route("Home/Index")]
[Route("Home/Index/{id?}")]
public IActionResult Index(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[Route("Home/About")]
[Route("Home/About/{id?}")]
public IActionResult About(int? id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

保留关键字

  • action
  • area
  • controller
  • handler
  • page

这些关键词是保留的路由参数名,在定义路由时,不能使用这些关键词。

HTTP 谓词模板

路由模板

路由模板用于定义路由匹配的模板,它分为

谓词模板示例

假设如下控制器:

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
[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
[HttpGet] // GET /api/test2
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")] // GET /api/test2/xyz
public IActionResult GetProduct(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[HttpGet("int/{id:int}")] // GET /api/test2/int/3
public IActionResult GetIntProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}

[HttpGet("int2/{id}")] // GET /api/test2/int2/3
public IActionResult GetInt2Product(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

在上述代码中:

  • 每个操作都包含 [HttpGet] 属性,该属性仅将匹配限制为 HTTP GET 请求。

  • GetProduct 操作包含 "{id}" 模板,因此 id 被附加到控制器上的 "api/[controller]" 模板中。 方法模板为 "api/[controller]/"{id}""。 因此,此操作仅匹配 /api/test2/xyz、/api/test2/123、/api/test2/{any string} 等形式的 GET 请求。

    1
    2
    3
    4
    5
    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
    return ControllerContext.MyDisplayRouteInfo(id);
    }
  • GetIntProduct 操作包含 "int/{id:int}") 模板。 模板的 :int 部分将 id 路由值限制为可以转换为整数的字符串。

    1
    2
    3
    4
    5
    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
    return ControllerContext.MyDisplayRouteInfo(id);
    }

    对于 /api/test2/int/abc 的 GET 请求,将会无法匹配到路由,并返回 404 Not Found 错误

  • GetInt2Product 操作在模板中包含 {id},但不将 id 限制为可以转换为整数的值。 对于 /api/test2/int2/abc 的 GET 请求,处理如下:

    1. 与此路由匹配。

    2. 模型绑定无法将 abc 转换为整数。 该方法的 id 参数是整数。

    3. 返回 400 Bad Request,因为模型绑定未能将 abc 转换为整数。

    1
    2
    3
    4
    5
    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
    return ControllerContext.MyDisplayRouteInfo(id);
    }

生成 REST API 时,很少需要在 操作方法 上使用 [Route(...)] ,因为该操作接受所有 HTTP 方法。 建议使用更具体的 HTTP 谓词属性来明确 API 所支持的操作。 API 的 REST 客户端应知道哪些路径和 HTTP 谓词映射到特定的逻辑操作。

REST API 应使用属性路由将应用的功能建模为一组资源,其中操作由 HTTP 谓词表示。 也就是说,对同一逻辑资源执行的许多操作(例如,GET 和 POST)都使用相同 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[ApiController]
public class MyProductsController : ControllerBase
{
[HttpGet("/products3")]
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpPost("/products3")]
public IActionResult CreateProduct(MyProduct myProduct)
{
return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
}
}

上述代码中的 URL 路径为 /products3:

  • 当 HTTP 谓词 为 GET 时,调用 MyProductsController.ListProducts。
  • 当 HTTP 谓词 为 POST 时,调用 MyProductsController.CreateProduct 。

特性路由组合

在控制器上定义的所有路由模板均作为操作上路由模板的前缀。在控制器上放置的路由特性会使控制器中的所有操作都使用该特性路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
[HttpGet]
public IActionResult ListProducts()
{
return ControllerContext.MyDisplayRouteInfo();
}

[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

在上面的示例中:

  • URL 路径 /products 可以匹配 ProductsApi.ListProducts
  • URL 路径 /products/5 可以匹配 ProductsApi.GetProduct(int)。

这两项操作仅匹配 HTTP GET,因为它们标记了 [HttpGet]。

操作上以 / 或 ~/ 开头的路由模板不与控制器的路由模板合并。

Attribute 与 [Route("Home")] 结合 定义路由模板
[Route("")] 是 "Home"
[Route("Index")] 是 "Home/Index"
[Route("/")] 否 ""
[Route("About")] 是 "Home/About"

特性路由继承

在父类控制器上定义的路由特性会继承给子类,可以在父类中定义一个通用的路由特性,减少在子类的控制器上重复定义。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
[ApiController]
[Route("[controller]/[action]")]
public abstrct class CustomBaseController : ControllerBase
{
}

// 此处可不进行路由特性定义
public class ProductsApiController : CustomBaseController
{
[HttpGet]
public IActionResult Index(){}
}

标记替换

特性路由支持标记替换,将标记用方括号([、])括起来即可。 标记 [action]、[area] 和 [controller] 会替换成定义了路由的操作中的操作名称、区域名称和控制器名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Products0:控制器名称
[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
// List:操作名称
[HttpGet]
public IActionResult List()
{
return ControllerContext.MyDisplayRouteInfo();
}


[HttpGet("{id}")]
public IActionResult Edit(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

Areas是一项 MVC 功能,用于将相关功能作为一个单独的组组织到一个组中,单击链接可跳转阅读更加详细的内容

标记样式转换

[controller],[action] 等会默认使用定义的名称作用 URL,而在实际开发中,我们可能需要将 PascalCase 命名转换成 hyphenCase 命名,如将 FindAll 变成 find-all。

可以通过实现 IOutboundParameterTransformer 接口来自定义。

接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Text.RegularExpressions;

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value == null) { return null; }

return Regex.Replace(value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}

使用:

1
2
3
4
5
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});

RouteTokenTransformerConvention 是应用程序的模型约定,可以:

  1. 将参数转换程序应用到程序中的所有特性路由中。

  2. 在替换特性路由标记值时对其进行自定义

多个路由特性

同一个控制器或者路由上,可以同时添加多个路由特性标记。

1
2
3
4
5
6
7
8
9
10
11
[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
[HttpPost("Buy")] // Matches 'Products6/Buy' and 'Store/Buy'
[HttpPost("Checkout")] // Matches 'Products6/Checkout' and 'Store/Checkout'
public IActionResult Buy()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

一般不要使用多个路由特性,会让 URL 看起来不易于理解,且容易冲突。

可选参数、默认值和约束

特性路由支持使用与传统路由相同的内联语法,来指定可选参数、默认值和约束。

1
2
3
4
5
6
7
8
public class Products14Controller : Controller
{
[HttpPost("{controller=ProductsDefault}/{id:int:string}/{name?}")]
public IActionResult ShowProduct(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}

使用说明:

  1. = 赋予默认值
  2. : 进行约束,可以同时使用多个约束
  3. ? 表示可选参数

内置路由约束:

约束 示例 匹配项示例 说明
int {id:int} 123456789, -123456789 匹配任何整数
bool {active:bool} true, FALSE 匹配 true 或 false。 不区分大小写
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm 在固定区域性中匹配有效的 DateTime 值。 请参阅前面的警告。
decimal {price:decimal} 49.99, -1,000.01 在固定区域性中匹配有效的 decimal 值。 请参阅前面的警告。
double {weight:double} 1.234, -1,001.01e8 在固定区域性中匹配有效的 double 值。 请参阅前面的警告。
float {weight:float} 1.234, -1,001.01e8 在固定区域性中匹配有效的 float 值。 请参阅前面的警告。
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 匹配有效的 Guid 值
long {ticks:long} 123456789, -123456789 匹配有效的 long 值
minlength(value) {username:minlength(4)} Rick 字符串必须至少为 4 个字符
maxlength(value) {filename:maxlength(8)} MyFile 字符串不得超过 8 个字符
length(length) {filename:length(12)} somefile.txt 字符串必须正好为 12 个字符
length(min,max) {filename:length(8,16)} somefile.txt 字符串必须至少为 8 个字符,且不得超过 16 个字符
min(value) {age:min(18)} 19 整数值必须至少为 18
max(value) {age:max(120)} 91 整数值不得超过 120
range(min,max) {age:range(18,120)} 91 整数值必须至少为 18,且不得超过 120
alpha {name:alpha} Rick 字符串必须由一个或多个字母字符组成,a-z,并区分大小写。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 字符串必须与正则表达式匹配。 请参阅有关定义正则表达式的提示。
required {name:required} Rick 用于强制在 URL 生成过程中存在非参数值

自定义特性路由

所有路由属性都实现 IRouteTemplateProvider。 ASP.NET Core 运行时:

  1. 应用启动时,在控制器类和操作方法上查找属性。
  2. 使用实现 IRouteTemplateProvider 的属性来构建初始路由集。

每个 IRouteTemplateProvider 都允许定义一个包含自定义路由模板、顺序和名称的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider
{
public string Template => "api/[controller]";
public int? Order => 2;
public string Name { get; set; } = string.Empty;
}

[MyApiController]
[ApiController]
public class MyTestApiController : ControllerBase
{
// GET /api/MyTestApi
[HttpGet]
public IActionResult Get()
{
return ControllerContext.MyDisplayRouteInfo();
}
}

上述 Get 方法返回 Order = 2, Template = api/MyTestApi。

路由返回值

ASP.NET Core 使用以下类型作为 Web API 控制器的操作返回类型:

请点击 ASP.NET Core Web API 中控制器操作的返回类型 进行详细阅读

传统路由与特性路由对比

类型 传统路由 特性路由
定义方式 在 Program.cs 中调用 MapControllerRoute 建立 URL 映射 在每个 Controller 中通过特性来定义 URL 映射
操作性 更简洁 要对每个 action 进行定义

路由快速配置

当理解了路由相关知识后,需要可以快速应用到实际项目中,本节记录一些快速配置代码,方便进行初始化。

映射路由

1
2
3
4
// ...
// 在 Run 之前调用 MapControllers 进行映射
app.MapControllers();
app.Run();

设置 hyphenCase 路由

增加 SlugifyParameterTransformer 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Text.RegularExpressions;

public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value == null) { return null; }

return Regex.Replace(value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}

Program.cs 中配置

1
2
3
4
5
builder.Services.AddControllersWithViews(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});

新建路由基类

所有子类都继承自这个基类

1
2
3
4
5
[Route("api/v1/[controller]")]
[ApiController]
public class CustomControllerBase: ControllerBase
{
}

参考

在 ASP.NET Core 中路由到控制器操作 | Microsoft Learn

ASP.NET Core 中的路由 | Microsoft Learn

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

ASP.NET Core 中的模型绑定 | Microsoft Learn