利用 HttpClient 进行网络请求并获取进度信息
.NET 下主要有三种方式可以进行网络请求相关操作,它们分别是
HttpWebRequest,WebClient 和
HttpClient。这三者的关系是不断递进发展的,所以一般使用
HttpClient 来进行网络请求。
本文主要讲述 HttpClient 的使用方法和进度获取。
.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 中反向代理的配置不全,导致怎么配置都有问题。这两天觉都没睡好,唉,坑是真的多呀,心累,不折腾了~
本文主要介绍 minio-js
的正确安装与使用。在网上搜了好久,都没有找到一个能正常运行
minio-js 的使用教程,包括官网。所以本文对此进行总结。
本文讲述了如何在 .NET Core 的项目中从零开始搭建单元测试,然后达到项目应用的程度。通过本文,你可以 get 以下知识:
我们在使用一种技术时,往往需要对现有技术调研,通过比较最终确定使用哪个。.NET 官方推荐的单元测试有 3 种:xUnit、NUnit、MSTest。
除了标注测试类和方法的特性用的不一样之外,它们是非常相似的。而 MSTest 与 VisualStudio 集成度更高,所以本人建议使用 MSTest。
StackOverflow 看到一条我很赞同的看法:
其实不用顾虑那么多,随便选择吧,MSTest 对 VS 的集成是最好的,而且也很容易上手,如果哪一天碰到它所无法解决的事情,切换到其他框架也非常简单,仅仅只是Nuget下个包,换下特性而已。
在 VS 中,选中方法名,右键 -> 创建单元测试,点击确定。
通过上述步骤,VS 会自动创建一个单元测试项目,在该项目里面自动生成单元测试内容。
1 | // 标记测试类 |
ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,并且默认注入了很多服务,具体可以参考 官方文档, 相信只要使用过依赖注入框架的同学,都会对此有不同深入的理解,在此无需赘言。
然而,在引入 IOC 框架之后,对于之前常规的对于类的依赖(new Class)变成通过构造函数对于接口的依赖(ASP.NET CORE 默认注入方式),这本身更加符合依赖倒置原则,但是对于单元测试来说确会带来另一个问题:
由于层层依赖,导致在某个类的方法进行测试的时候,需要构造一大堆该类依赖的接口的实现,非常麻烦。
这个时候,我们脑子里会下意识想一个问题:为什么常用的 .Net 单元测试框架不支持依赖注入?
于是笔者带着这个问题在查阅了一些关于在单元测试中支持依赖注入的讨论Github Issue,以及其他的相关文档,突然明白一个之前一直忽视但实际却非常重要的问题:
在对于一个方法的单元测试中,我们应该关注的是这个方法内部的逻辑测试,而这个方法内部对于外部的依赖,则不在这个单元测试关注的范围内
换言之,单元测试永远都只关注需要测试的方法内部的逻辑实现,至于外部依赖方法的测试,则应该放在另一个专门针对这个方法的单元测试用例中。
弄清楚这个问题,我们才能更加理解另一个单元测试不可缺少的框架——Mock框架。在我们写的测试中,应该忽略外部依赖具体的实现,而是通过模拟该接口方法来显示的指定返回值,从而降低该返回值对于当前单元测试结果的影响,而 Mock 框架(例如最常用的Moq),刚好可以满足我们对于接口的模拟需求。
相信有同学跟我有同样的疑惑,并且当我尝试在 ASP.NET Core 单元测试中的一切外部依赖通过 Mock 的方式进行编写的时候,遇到了一些问题,下文会将这些问题一一道来,希望对有同样疑惑的同学有所帮助。
在 .NET 中有几种 mock 框架可供选择,比如 NMock、PhinoMocks、FakeItEasy和Moq。尽管Moq相对较新,但是它非常易用。不需要像传统的 Record/Replay。并且使用 Moq 在 VS 中可以得到智能提示。学习成本也不高。
所以选择 Moq 作为 Mock 数据框架。Moq 有一个自动 Mock 库 Moq.AutoMock,建议安装该库。
1 | var mock = new Mock<ILoveThisLibrary>(); |
上面的方式可以简化成:
1 | ILoveThisLibrary lovable = Mock.Of<ILoveThisLibrary>(l => |
简而言之,Mock 数据的使用步骤可总结如下:
mockmock 设置方法的返回值mock.Object 获取 Mock
的对象来传递给目标方法使用1 | var mocker = new AutoMocker(); |
1 | var mocker = new AutoMocker(); |
例如在编写 Repository 层进行单元测试时,经常有同学会编写依赖于数据库数据的单元测试,这样并不利于随时随地的进行单元测试检查。
如果将该流程放在 CI/CD 中,在代码的发布过程中通过单元测试可以检查代码逻辑的正确性,同时依赖于数据库的单元测试将不会通过(通常情况下,生产环境和开发环境隔离),变相迫使开发小伙伴通过 mock 方式模拟数据库返回结果。
这个原则同样适用于不能依赖三方API编写单元测试。
CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题(亦称:"集成地狱")。
通常很多开发 Leader 都会要求开发团队编写单元测试,但是很少检查单元测试的质量,即单元测试最重要的指标——单元测试代码覆盖率,如果不注重覆盖率的提升,那么很有可能会导致开发成员为了单元测试而写单元测试,预期就会与实际情况相差甚远。
保证单元测试代码覆盖率,将会大大降低代码变更带来的 Bug 率,从而节省整体开发成本。
对于初次开始编写单元测试的开发人员,脑中经常会对此表示怀疑:我为什么要去验证一堆我自己写的正确的逻辑?
实际这个问题包含了区分一个一般开发人员和优秀开发人员很重要的一个条件:他是否会反向思考当前逻辑的正确性。有了这种思维,看待问题才会从多个角度入手分析,对问题的本质掌握更加全面。
不要怀疑,坚持写单元测试,因为这本身也是对反向思维的一种锻炼,以笔者的经验,只有当编写过一段时间之后,才会真正认识单元测试的魅力,并且开始非常习惯的在写一段逻辑之后,顺手写了对于它的单元测试。
即使笔者也算很早就开始写单元测试了,但直到写这篇文章,仍然不断在加深对单元测试的认识。
本文主要记录 Swagger 在使用过程中遇到的一些问题,从而避免再次踩坑。
本文主要介绍如何在 .NET Core 项目中配置基于 JWT 的 Token 验证。
本文总结了在 .NetCore 中使用 Minio 的过程中遇到的一些问题。
本文总结了在 .NetCore 中使用 Minio 的过程中遇到的一些问题。
在将 mongodb 中的数据映射到类的时候,当 mongodb
中的字段与定义的实体类的字段不一样时,会报错,此时只需要在类上添加
BsonIgnoreExtraElements 特性,即可解决。
ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。本文以使用者的角度,对路由的使用进行概括说明,方便知识回顾与使用。
ASP.NET Core 控制器使用路由中间件来匹配传入请求的 URL 并将它们映射到操作。它支持传统路由,也支持属性路由。如果感觉到陌生,不要着急,继续向下看,下面会一一道来。
传统路由通常在 MVC 框架中使用。
它在 program.cs 中定义,如下:
完整方法:
1 | app.MapControllerRoute( |
简化使用:
1 | app.MapDefaultControllerRoute(); |
说明:
上面的完整路由定义中:
第一个路径段 {controller=Home}
映射到控制器名称。
如 UserController 中的控制器名为
User。
第二段 {action=Index} 映射到操作名称。
action 就是 Controller 类中的方法名。
第三段 {id?} 用于可选 id。
{id?} 中的 ? 使其成为可选。 id
用于映射到模型实体。
1 | public class HomeController : Controller |
可以多次调用 MapControllerRoute
来设置多个传统路由,如下:
1 | app.MapControllerRoute(name: "blog", |
上述代码中的 blog 路由是专用的传统路由。 之所以称为专用传统路由是因为
controller 和 action 不会以参数形式出现在路由模板 "blog/{*article}"
中,它们只能具有默认值 { controller = "Blog", action = "Article"
}。因此,此路由将会始终映射到操作
BlogController.Article。
按定义顺序匹配
具体的路由在可变路由之前匹配
比如 users/demo 会在 users/{userId}
之前进行匹配
Attribute 本应翻译成属性,但为了与 .NET 中的属性字段区分,本文称之为特性。
特性路由通常在 REST API 中使用。
1 | var builder = WebApplication.CreateBuilder(args); |
属性路由通过调用 MapControllers
来映射属性路由控制器。
下面的示例中,HomeController 匹配一组类似于默认传统路由
{controller=Home}/{action=Index}/{id?} 匹配的 URL。
1 | public class HomeController : Controller |
actionareacontrollerhandlerpage这些关键词是保留的路由参数名,在定义路由时,不能使用这些关键词。
路由模板用于定义路由匹配的模板,它分为
假设如下控制器:
1 | [Route("api/[controller]")] |
在上述代码中:
每个操作都包含 [HttpGet] 属性,该属性仅将匹配限制为
HTTP GET 请求。
GetProduct 操作包含 "{id}" 模板,因此
id 被附加到控制器上的 "api/[controller]"
模板中。 方法模板为 "api/[controller]/"{id}""。
因此,此操作仅匹配
/api/test2/xyz、/api/test2/123、/api/test2/{any string}
等形式的 GET 请求。
1 | [HttpGet("{id}")] // GET /api/test2/xyz |
GetIntProduct 操作包含 "int/{id:int}")
模板。 模板的 :int 部分将 id
路由值限制为可以转换为整数的字符串。
1 | [HttpGet("int/{id:int}")] // GET /api/test2/int/3 |
对于 /api/test2/int/abc 的 GET
请求,将会无法匹配到路由,并返回 404 Not Found
错误
GetInt2Product 操作在模板中包含
{id},但不将 id 限制为可以转换为整数的值。
对于 /api/test2/int2/abc 的 GET 请求,处理如下:
与此路由匹配。
模型绑定无法将 abc 转换为整数。 该方法的
id 参数是整数。
返回 400 Bad
Request,因为模型绑定未能将 abc 转换为整数。
1 | [HttpGet("int2/{id}")] // GET /api/test2/int2/3 |
生成 REST API 时,很少需要在 操作方法 上使用
[Route(...)] ,因为该操作接受所有 HTTP 方法。
建议使用更具体的 HTTP 谓词属性来明确 API 所支持的操作。 API 的 REST
客户端应知道哪些路径和 HTTP 谓词映射到特定的逻辑操作。
REST API 应使用属性路由将应用的功能建模为一组资源,其中操作由 HTTP 谓词表示。 也就是说,对同一逻辑资源执行的许多操作(例如,GET 和 POST)都使用相同 URL。
1 | [ApiController] |
上述代码中的 URL 路径为 /products3:
GET 时,调用
MyProductsController.ListProducts。POST 时,调用
MyProductsController.CreateProduct 。在控制器上定义的所有路由模板均作为操作上路由模板的前缀。在控制器上放置的路由特性会使控制器中的所有操作都使用该特性路由。
1 | [ApiController] |
在上面的示例中:
/products 可以匹配
ProductsApi.ListProducts/products/5 可以匹配
ProductsApi.GetProduct(int)。这两项操作仅匹配 HTTP GET,因为它们标记了
[HttpGet]。
操作上以 / 或 ~/
开头的路由模板不与控制器的路由模板合并。
| Attribute | 与 [Route("Home")] 结合 |
定义路由模板 |
|---|---|---|
[Route("")] |
是 | "Home" |
[Route("Index")] |
是 | "Home/Index" |
[Route("/")] |
否 | "" |
[Route("About")] |
是 | "Home/About" |
在父类控制器上定义的路由特性会继承给子类,可以在父类中定义一个通用的路由特性,减少在子类的控制器上重复定义。
例如:
1 | [ApiController] |
特性路由支持标记替换,将标记用方括号([、])括起来即可。
标记 [action]、[area] 和
[controller]
会替换成定义了路由的操作中的操作名称、区域名称和控制器名称。
1 | // Products0:控制器名称 |
[controller],[action]
等会默认使用定义的名称作用 URL,而在实际开发中,我们可能需要将
PascalCase 命名转换成 hyphenCase 命名,如将 FindAll 变成
find-all。
可以通过实现 IOutboundParameterTransformer 接口来自定义。
接口实现:
1 | using System.Text.RegularExpressions; |
使用:
1 | builder.Services.AddControllersWithViews(options => |
RouteTokenTransformerConvention 是应用程序的模型约定,可以:
将参数转换程序应用到程序中的所有特性路由中。
在替换特性路由标记值时对其进行自定义
同一个控制器或者路由上,可以同时添加多个路由特性标记。
1 | [Route("Store")] |
一般不要使用多个路由特性,会让 URL 看起来不易于理解,且容易冲突。
特性路由支持使用与传统路由相同的内联语法,来指定可选参数、默认值和约束。
1 | public class Products14Controller : Controller |
使用说明:
= 赋予默认值: 进行约束,可以同时使用多个约束? 表示可选参数内置路由约束:
| 约束 | 示例 | 匹配项示例 | 说明 |
|---|---|---|---|
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
运行时:
IRouteTemplateProvider
的属性来构建初始路由集。每个 IRouteTemplateProvider
都允许定义一个包含自定义路由模板、顺序和名称的路由:
1 | public class MyApiControllerAttribute : Attribute, IRouteTemplateProvider |
上述 Get 方法返回
Order = 2, Template = api/MyTestApi。
ASP.NET Core 使用以下类型作为 Web API 控制器的操作返回类型:
请点击 ASP.NET Core Web API 中控制器操作的返回类型 进行详细阅读
| 类型 | 传统路由 | 特性路由 |
|---|---|---|
| 定义方式 | 在 Program.cs 中调用 MapControllerRoute
建立 URL 映射 |
在每个 Controller 中通过特性来定义 URL 映射 |
| 操作性 | 更简洁 | 要对每个 action 进行定义 |
当理解了路由相关知识后,需要可以快速应用到实际项目中,本节记录一些快速配置代码,方便进行初始化。
1 | // ... |
增加 SlugifyParameterTransformer 类
1 | using System.Text.RegularExpressions; |
Program.cs 中配置
1 | builder.Services.AddControllersWithViews(options => |
所有子类都继承自这个基类
1 | [Route("api/v1/[controller]")] |
在 ASP.NET Core 中路由到控制器操作 | Microsoft Learn
ASP.NET Core 中的路由 | Microsoft Learn