|
| 1 | +# .NET 每周分享第 56 期 |
| 2 | + |
| 3 | +## 卷首语 |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +Microsoft Build 大会 .NET 内容[汇总](https://devblogs.microsoft.com/dotnet/dotnet-build-2024-announcements/)。 |
| 8 | + |
| 9 | +## 行业资讯 |
| 10 | + |
| 11 | +1、[API 文档源码查询](https://devblogs.microsoft.com/dotnet/dotnet-docs-link-to-source-code/) |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +之前我们分享过 `.NET` 官方文档中现在提供了 `GitHub` 源码查询的链接,这样大大方便了我们了解更多的实现细节。这篇文章介绍了 `.NET` 组是如何开发这个功能的。 |
| 16 | + |
| 17 | +## 文章推荐 |
| 18 | + |
| 19 | +1、[Aspire 和 docker 比较](https://www.growthaccelerationpartners.com/blog/simplifying-cloud-native-net-development-net-aspire-vs-docker) |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | +| 功能 | .NET Aspire | Docker | |
| 24 | +| --- | --- | --- | |
| 25 | +| 编排 | 提供用于管理应用程序组成、服务发现和本地开发环境连接字符串的抽象。这简化了复杂分布式应用程序的设置。 | 主要专注于容器运行时和管理。复杂应用程序的编排通常涉及额外的工具,如Docker Compose或Kubernetes。 | |
| 26 | +| 云原生组件 | 提供标准化与流行云服务(Redis、PostgreSQL、Azure Service Bus等)集成的NuGet包(组件)。这简化了设置和配置过程,包括健康检查、遥测和服务发现。 | 虽然Docker可以运行任何容器化服务,但与特定云服务的集成通常通过应用程序代码中的配置或通过单独的工具来处理。 | |
| 27 | +| 工具和模板 | 提供适用于Visual Studio和dotnet CLI的项目模板和工具,专为云原生应用程序开发而设计。这包括默认配置和服务的样板代码,如健康检查、遥测和服务发现,加速开发。 | 主要是命令行工具(docker)和相关工具(Docker Compose等)。与IDE的集成可能需要额外的扩展或插件。 | |
| 28 | +| 目标用户 | 主要是构建云原生应用程序的.NET开发人员。它假设了解.NET概念,并提供更高层次的抽象,以简化云原生开发体验。 | 从事容器化应用程序的开发人员、DevOps工程师和系统管理员。Docker需要对容器及其管理有更深入的了解。 | |
| 29 | +| 生态系统 | 与.NET生态系统和Microsoft Azure云服务紧密集成。 | 一个被广泛采用的行业标准,拥有支持容器化的庞大工具、平台和服务生态系统。 | |
| 30 | + |
| 31 | +2、[ASP.NET Core 中的短路](https://dev.to/moh_moh701/introduction-to-shortcircuit-and-mapshortcircuit-in-net-8-12ml) |
| 32 | + |
| 33 | + |
| 34 | + |
| 35 | +在 `ASP.NET Core` 中每个请求的处理流程如下: |
| 36 | +- 找到相应的 route 定义 |
| 37 | +- 链式处理每个中间件 |
| 38 | +- 找到对应的 `Endpoint` 并且处理 |
| 39 | + |
| 40 | +在 `ASP.NET Core 8` 中引入的 `Short Circuit`。顾名思义,我们定义好一些请求可以不用经过各种 `Middleware` 而直接处理并且返回,比如像一些静态文件处理,我们可以直接返回结果,通常是为了下述考虑: |
| 41 | + |
| 42 | +- 性能优化 |
| 43 | +- 资源有效性 |
| 44 | +- 简化路由 |
| 45 | + |
| 46 | +```csharp |
| 47 | +var builder = WebApplication.CreateBuilder(args); |
| 48 | +var app = builder.Build(); |
| 49 | +app.UseMiddleware<CustomerMiddleware>(); |
| 50 | +app.MapGet("/shortcircuit", () => "Hello From Short Circuit").ShortCircuit(); |
| 51 | +app.Run(); |
| 52 | + |
| 53 | +class CustomerMiddleware |
| 54 | +{ |
| 55 | + private readonly RequestDelegate _next; |
| 56 | + |
| 57 | + public CustomerMiddleware(RequestDelegate next) |
| 58 | + { |
| 59 | + _next = next; |
| 60 | + } |
| 61 | + public Task Invoke(HttpContext context) |
| 62 | + { |
| 63 | + System.Console.WriteLine("Customer Middleware"); |
| 64 | + return _next(context); |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +当我们访问 `shortcircuit` 路径的时候,`CustomerMiddleware` 并不会执行。除此之外,还有 `MapShortCircuit` 拓展方法 |
| 70 | + |
| 71 | +```csharp |
| 72 | +app.MapShortCircuit(500, "robots.txt", "favicon.ico"); |
| 73 | +``` |
| 74 | + |
| 75 | +这里定义了所有访问 `robots.txt` 和 `favicon.ico` 路径的请求都会返回 500. |
| 76 | + |
| 77 | +3、[6 条 C# 性能提升的建议](https://www.code4it.dev/blog/top-6-string-performance-tips/) |
| 78 | + |
| 79 | +1. StringBuilder Vs String concatenation |
| 80 | +- 如果只有几个字符串,使用字符串拼接 |
| 81 | +- 如果有很多字符串,使用 `StringBuilder` |
| 82 | + |
| 83 | +2. EndsWith(string) Vs EndsWith(char) |
| 84 | + |
| 85 | +`EndsWith(char)` 性能比较好 |
| 86 | + |
| 87 | +3. IsNullOrEmpty Vs IsNullOrWHitespace Vs IsNullOrEmpty + Trim |
| 88 | + |
| 89 | +- `StringIsNullOrWhitespace` 性能比 `StringIsNullOrEmpty` 慢 |
| 90 | +- 如果数据是来在外部,使用 `StringIsNullOrWhitespace` |
| 91 | +- 如果认为 `\n \n\t` 是合法字符,使用 `StringIsNullOrEmpty` |
| 92 | + |
| 93 | +4. ToUpper Vs ToUpperInvariant 和 ToLower Vs ToLowerInvariant |
| 94 | + |
| 95 | +- `ToUpper` 方法通常比 `ToLower` 慢 |
| 96 | +- `Invariant` 方法通常比 `non-invariant` 慢 |
| 97 | + |
| 98 | +5. OrdinalIgnoreCase Vs InvariantCultureIgnoreCase |
| 99 | + |
| 100 | +- `InvariantCultureIgnoreCase` 比 `OrdinalIgnoreCase` 慢, 因为 `InvariantCultureIgnoreCase` 检查字符串的意义 |
| 101 | + |
| 102 | +```csharp |
| 103 | +var s1 = = "Straße"; // German for "street" |
| 104 | +var s2 ="STRASSE"; |
| 105 | + |
| 106 | +string.Equals(s1, s2, StringComparison.InvariantCultureIgnoreCase); //True |
| 107 | +string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase); //False |
| 108 | +``` |
| 109 | + |
| 110 | +6. Newtonsoft Vs System.Text.Json |
| 111 | + |
| 112 | +`System.Text.Json` 在时间和内存消耗上比 `Newtonsoft` 有更好的表现 |
| 113 | + |
| 114 | +4、[不使用 Interface 而完成单元测试的方法](https://www.code4it.dev/blog/unit-tests-without-interfaces/) |
| 115 | + |
| 116 | + |
| 117 | + |
| 118 | +对于单元测试,通常的做法是将一些外部依赖抽象成接口,这样可以通过接口不同的实现来进行单元测试。但是 `C#` 除了接口,还可以通过其他方式完成单元测试: |
| 119 | + |
| 120 | +1. Virtual |
| 121 | + |
| 122 | +C# 中 `Virtual` 关键字用来表示该属性或者方法可以在子类中进行重载,所以在单元测试中可以构造新的子类来重写部分方法。 |
| 123 | + |
| 124 | +```csharp |
| 125 | +public class NumbersRepository |
| 126 | +{ |
| 127 | + private readonly int[] _allNumbers; |
| 128 | + public NumbersRepository(){ |
| 129 | + _allNumbers = Enumerable.Range(0, 100).ToArray(); |
| 130 | + } |
| 131 | + public virtual IEnumerable<int> GetNumbers() => Random.Shared.GetItems(_allNumbers, 50); |
| 132 | +} |
| 133 | + |
| 134 | +public class NumbersSearchService |
| 135 | +{ |
| 136 | + private readonly NumbersRepository _repository; |
| 137 | + public NumbersSearchService(NumbersRepository repository) { |
| 138 | + _repository = repository; |
| 139 | + } |
| 140 | + public bool Contains(int number){ |
| 141 | + var numbers = GetNumbers(); |
| 142 | + return numbers.Contains(number); |
| 143 | + } |
| 144 | + public IEnumerable<int> GetNumbers() => _repository.GetNumbers(); |
| 145 | +} |
| 146 | + |
| 147 | +// 单元测试 |
| 148 | +internal class StubNumberRepo : NumbersRepository |
| 149 | +{ |
| 150 | + private IEnumerable<int> _numbers; |
| 151 | + public void SetNumbers(params int[] numbers) => _numbers = numbers; |
| 152 | + public override IEnumerable<int> GetNumbers() => _numbers; |
| 153 | +} |
| 154 | + |
| 155 | +[TestMethod] |
| 156 | +public void Should_WorkWithStubRepo() { |
| 157 | + // Arrange |
| 158 | + var repository = new StubNumberRepo(); |
| 159 | + repository.SetNumbers(1, 2, 3); |
| 160 | + var service = new NumbersSearchService(repository); |
| 161 | + // Act |
| 162 | + var result = service.Contains(3); |
| 163 | + // Assert |
| 164 | + Assert.AreEqual(result, true); |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +2. New |
| 169 | + |
| 170 | +C# 中 `new` 关键字也能隐藏父类的方法和属性,这样我们只需要测试其他内容就可以完成单元测试 |
| 171 | + |
| 172 | +```csharp |
| 173 | +public class NumbersSearchService { |
| 174 | + private readonly NumbersRepository _repository; |
| 175 | + public NumbersSearchService(NumbersRepository repository) { |
| 176 | + _repository = repository; |
| 177 | + } |
| 178 | + public bool Contains(int number) { |
| 179 | + var numbers = GetNumbers(); |
| 180 | + return numbers.Contains(number); |
| 181 | + } |
| 182 | + public IEnumerable<int> GetNumbers() => _repository.GetNumbers(); |
| 183 | +} |
| 184 | + |
| 185 | +internal class StubNumberSearch : NumbersSearchService { |
| 186 | + private IEnumerable<int> _numbers; |
| 187 | + private bool _useStubNumbers; |
| 188 | + |
| 189 | + public void SetNumbers(params int[] numbers) { |
| 190 | + _numbers = numbers.ToArray(); |
| 191 | + _useStubNumbers = true; |
| 192 | + } |
| 193 | + |
| 194 | + public new IEnumerable<int> GetNumbers() => _useStubNumbers ? |
| 195 | + _numbers:base.GetNumbers(); |
| 196 | +} |
| 197 | +``` |
| 198 | + |
| 199 | +在单元你测试中只需要测试 `StubNumberSearch` 即可。 |
| 200 | + |
| 201 | +3. Moq |
| 202 | + |
| 203 | +`Moq` 是 `.NET` 社区广泛使用的单元测试框架,使用 `Moq` 可以很方便地构造测试子类 |
| 204 | + |
| 205 | +```csharp |
| 206 | +public class NumbersRepository { |
| 207 | + private readonly int[] _allNumbers; |
| 208 | + |
| 209 | + public NumbersRepository() { |
| 210 | + _allNumbers = Enumerable.Range(0, 100).ToArray(); |
| 211 | + } |
| 212 | + |
| 213 | + public virtual IEnumerable<int> GetNumbers() => Random.Shared.GetItems( |
| 214 | + _allNumbers, 50); |
| 215 | +} |
| 216 | + |
| 217 | +[TestMethod] |
| 218 | +public void Should_WorkWithMockRepo() { |
| 219 | + // Arrange |
| 220 | + var repository = new Moq.Mock<NumbersRepository>(); |
| 221 | + repository.Setup(_ => _.GetNumbers()).Returns(new int[]{1, 2, 3}); |
| 222 | + var service = new NumbersSearchService(repository.Object); |
| 223 | + |
| 224 | + // Act |
| 225 | + var result = service.Contains(3); |
| 226 | + |
| 227 | + // Assert |
| 228 | + Assert.AreEqual(result, true); |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +5、[Hanselman & Toub 现场代码秀](https://www.youtube.com/watch?v=TRFfTdzpk-M&ab_channel=MicrosoftDeveloper) |
| 233 | + |
| 234 | +今年 `Build` 大会,`Scott Hanselman` 和 `Stephen Toub` 这对老搭档又来整活,他们通过对 [Humanizer](https://github.com/Humanizr/Humanizer) 这个库的优化,来提升 C# 代码性能。 |
| 235 | + |
| 236 | +- Span |
| 237 | + |
| 238 | +```diff |
| 239 | +- value.Substring(0, length - truncationString.Length) + truncationString |
| 240 | ++ String.Concat(value.AspSpan(0, length - truncationString.Length), truncationString.AsSpan()) |
| 241 | +``` |
| 242 | + |
| 243 | +- ReadOnlySpan Parameter |
| 244 | + |
| 245 | +```diff |
| 246 | +- public static string Transform(this string input, params IStringTransformer[] transformers) |
| 247 | ++ public static string Transform(this string input params ReadonlySpan<IStringTransformer> transformers) |
| 248 | +``` |
| 249 | + |
| 250 | +- Regex |
| 251 | + |
| 252 | +```diff |
| 253 | +- private static readonly Regex ValidRomanNumeral = new Regex("^(?i:(?=[MDCLXVI])((M{0,3})((C[DM])|(D?C{0,3}))?((X[LC])|(L?XX{0,2})|L)?((I[VX])|(V?(II{0,2}))|V)?))$", RegexOptionsUtil.Compiled); |
| 254 | ++ [GeneratedRegex("^(?i:(?=[MDCLXVI])((M{0,3})((C[DM])|(D?C{0,3}))?((X[LC])|(L?XX{0,2})|L)?((I[VX])|(V?(II{0,2}))|V)?))$", RegexOptions.IgnroeCase)] |
| 255 | ++ private static partial Regex ValidRomanNumeral(); |
| 256 | +``` |
| 257 | + |
| 258 | +- Switch Pattern |
| 259 | + |
| 260 | +```diff |
| 261 | +- private static readonly IDictionary<string, int> RomanNumerals = |
| 262 | +- new Dictionary<string, int>(NumberOfRomanNumeralMaps){ |
| 263 | +- {"M", 1000}, {"D", 500}, {"C", 100}, {"L", 50}, {"X", 10}, , {"V", 5}, {"I", 1}}; |
| 264 | ++ private static int GetValue(char c) =>(c & 0x20) switch { |
| 265 | ++ 'M' => 1000, 'D' => 500, 'C' => 100, 'L' => 50, |
| 266 | ++ 'X' => 10, 'V' => 5, 'I' => 1 }; |
| 267 | +``` |
| 268 | + |
| 269 | +6、[升级的你的 .NET 应用程序,以 AWS 为例](https://www.youtube.com/watch?v=zb7v3d7IgYs) |
| 270 | + |
| 271 | + |
| 272 | + |
| 273 | +如果说你的应用程序是 `.NET Framework` + `SQL Server` 的架构,并且部署在 `On premise` 上,那么你的应用程序就像**囚犯**一样: |
| 274 | + |
| 275 | +- 受限于 `on-premise` 无法动态扩展 |
| 276 | +- 被 Windows 和 `SQL Server` 授权费用牵制 |
| 277 | +- 受困于服务器管理 |
| 278 | +- 受整体架构的限制 |
| 279 | + |
| 280 | +使用 `AWS` 和相关工作可以帮助解决上述问题 |
| 281 | + |
| 282 | +**阶段一** |
| 283 | + |
| 284 | +- SQL Server 迁移到 SQL Server on EC2 |
| 285 | +- 应用程序迁移到 Virtaul Machine (Amazon EC2) 或者 Windows Container (Amazon EKS) |
| 286 | + |
| 287 | +**阶段二** |
| 288 | + |
| 289 | +- 迁移 `.NET Framework` 到 `.NET Core` |
| 290 | +- 迁移 `SQL Server` 到开源数据库,比如 `Amazon Aurora`, `PostgreSQL` 或者 `MySQL` |
| 291 | + |
| 292 | +**阶段三** |
| 293 | + |
| 294 | +- 将服务进行微服务化,使用 `Linux Container` |
| 295 | +- 将数据库中特定服务中数据拆分,比如 `Amazon DynamoDB` |
| 296 | + |
| 297 | +**阶段四** |
| 298 | + |
| 299 | +- 云原生重构,比如 `Serverless`, `Event-driven` |
| 300 | + |
| 301 | +7、[当开源维护者不再更新 NuGet 包怎么办?](https://www.thereformedprogrammer.net/how-to-update-a-nuget-library-once-the-author-isnt-available/) |
| 302 | + |
| 303 | +这是一篇令人难受的文章,作者是一名 `.NET` 开源库的维护者。但是最近他被诊断为一种叫做 `失智` 的疾病,该疾病导致作者无法再及时更新这些开源库。 |
| 304 | +微软每年都会发布新的 `.NET` 版本,每个版本都包含新的功能,或者性能的提升,亦或者一些 breaking change。如果这个开源库只依赖 `.NET Standard`, 新发布的 `.NET` 并不会影响他们,以为只依赖 `BCL`。如果依赖特定的 `.NET`,但是使用者想再新的 `.NET` 版本中使用这个库,但是作者并没有及时更新这个库,那该怎么办呢? |
| 305 | + |
| 306 | +- 首先下载这个库的源码到本地,通常 `NuGet` 页面会有指向源码的链接 |
| 307 | +- 修改项目的 `TargetFramework` 属性到最新的版本 |
| 308 | +- 更新项目的依赖到最新版本 |
| 309 | +- 编译代码 |
| 310 | +- 运行单元测试 |
| 311 | +- 更新 `NuGet` 文件信息 |
| 312 | +- 创建本地 `.nupkg` 文件 |
| 313 | +- 将其加入到 `NuGet` 源 |
| 314 | + |
| 315 | +## 开源项目 |
| 316 | + |
| 317 | +1、[Aspire GA](https://devblogs.microsoft.com/dotnet/dotnet-aspire-general-availability/) |
| 318 | + |
| 319 | + |
| 320 | + |
| 321 | +最近在 `Build` 大会中,`Aspire` 这个项目已经处于 `GA` 状态,也就是可以可以尝试使用。 |
| 322 | + |
| 323 | +2、[ExcelMapper](https://github.com/mganss/ExcelMapper) |
| 324 | + |
| 325 | + |
| 326 | + |
| 327 | +`ExcelMapper` 库可以帮助我们非常方便的从 Excel 中读取数据,并且转换成相应的 POCO 对象集合。比如 Excel 中的数据如下 |
| 328 | + |
| 329 | + |
| 330 | + |
| 331 | +首先定义实体 |
| 332 | + |
| 333 | +```csharp |
| 334 | +class Product |
| 335 | +{ |
| 336 | + public string Name { get; set; } |
| 337 | + [Column("Number")] |
| 338 | + public int NumberInStock { get; set;} |
| 339 | + public decimal Price { get; set; } |
| 340 | +} |
| 341 | +``` |
| 342 | + |
| 343 | +使用 `Fetch` 方法获取对象集合 |
| 344 | + |
| 345 | +```csharp |
| 346 | +using Ganss.Excel; |
| 347 | +var products = new ExcelMapper("Product.xlsx").Fetch<Product>(); |
| 348 | +System.Console.WriteLine(products.Count()); |
| 349 | +``` |
| 350 | + |
| 351 | +3、[JMESPath.Net](https://github.com/jdevillard/JmesPath.Net) |
| 352 | + |
| 353 | +JMESPath 是一种针对 JSON 的查询语言。您可以从 JSON 文档中提取和转换元素。`JMESPath.Net` 是一个 `C#` 实现的库,可以帮助我们在 `C#` 中使用 `JMESPath` 查询语言。 |
| 354 | + |
| 355 | +4、[OFGB](https://github.com/xM4ddy/OFGB) |
| 356 | + |
| 357 | + |
| 358 | + |
| 359 | +Window 11 在最新的一次更新中,包含了大量的广告,比如文件管理系统,开始菜单等等。这个开源工具可以一键关闭这些功能。 |
0 commit comments