|
| 1 | +# .NET 每周分享第 42 期 |
| 2 | + |
| 3 | +## 卷首语 |
| 4 | + |
| 5 | +为什么 Startup 不选择 .NET 作为后端语言 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +`Reddit` 上的一个[讨论](https://www.reddit.com/r/dotnet/comments/16fu7o0/why_isnt_dotnet_core_popular_among_startups/?share_id=m43b3lhRz0cwGG0GLrSew&utm_content=1&utm_medium=ios_app&utm_name=ioscss&utm_source=share&utm_term=1&rdt=61720) |
| 10 | + |
| 11 | +> 为什么创业公司不选择 .NET |
| 12 | +
|
| 13 | +高赞回答列出了下面几个原因 |
| 14 | + |
| 15 | +1. 创业公司吸引年轻人,而年轻人更加喜欢 Javascript |
| 16 | +2. 创业公司通常是全栈,Javascript 适合全栈开发 |
| 17 | +3. 对于 `C#` 和 `.NET` 有着传统的看法 |
| 18 | +4. 过去一段事件 `.NET` 社区一些错误决定 |
| 19 | +5. `.NET` 这种基于约束的方法很难让人喜欢 |
| 20 | +6. 人力资源方面的挑战 |
| 21 | +7. 不喜欢多线程 |
| 22 | +8. 面向对象编程的恐惧 |
| 23 | +9. 创业公司喜欢 Mac 电脑 |
| 24 | +10. 认为 `.NET` 和 `Java` 太主流了 |
| 25 | + |
| 26 | + |
| 27 | +## 行业资讯 |
| 28 | + |
| 29 | +1、[C# 学习证书](https://devblogs.microsoft.com/dotnet/csharp-certification-training-series/) |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | +微软联合 `freecCodeCamp` 推出了 `C#` 培训课程,在其中可以获得相关的证书。 |
| 34 | + |
| 35 | +2、[Visual Studio 团队茶话会](https://devblogs.microsoft.com/visualstudio/visual-studio-tea-technology-miniseries/) |
| 36 | + |
| 37 | + |
| 38 | + |
| 39 | +10 月 4 号,`Visual Studio` 团队会组织一个茶话会,邀请团队人来分享关于 `Visual Studio` 的其他不为人知的方面,比如如何将 idea 转换为 feature,如何 debug,如何用 Visual Studio 创建移动应用程序。 |
| 40 | + |
| 41 | + |
| 42 | +## 文章推荐 |
| 43 | + |
| 44 | +1、[EF Core 中的乐观锁](https://www.milanjovanovic.tech/blog/solving-race-conditions-with-ef-core-optimistic-locking) |
| 45 | + |
| 46 | + |
| 47 | + |
| 48 | + |
| 49 | +在数据库应用程序中,我们通常需要考虑并发问题。比如一个酒店订阅系统,我们不能在同一个时间段将一个房间分发给两个订单。应用程序的逻辑一般是这样的 |
| 50 | + |
| 51 | +1. 查询特定房间在特定时间段的订阅情况 |
| 52 | +2. 如果被预定,返回订阅失败 |
| 53 | +3. 否则,调用付款等其他服务 |
| 54 | +4. 更新房间信息并且保存 |
| 55 | + |
| 56 | +这些步骤不是在同一个 `Transaction` 完成的,这就带来一个问题,如果在第 1 步之后,房间被其他用户约定了,那么在第 4 步的时候,就会覆盖前面一个订阅的结果。 |
| 57 | +那么 `SQL Server` 中的乐观锁就能解决这个问题,对于表中的都会内置一个 `rowversion` 列,当这一行被更新后,就会自动更改,那么过程就变成这样 |
| 58 | +1. 查询特定房间在特定时间段的订阅情况 |
| 59 | +2. 如果被预定,返回订阅失败 |
| 60 | +3. 否则或者该行数据和 `rowversion`,调用付款等其他服务 |
| 61 | +4. 在更新的时候只有 `rowversion` 和之前上一步获取的相同的时候,才更新成功,否则失败。 |
| 62 | + |
| 63 | +体现在代码中是这样的 |
| 64 | + |
| 65 | +```csharp |
| 66 | +public class Apartment |
| 67 | +{ |
| 68 | + public Guid Id { get; set; } |
| 69 | + [Timestamp] |
| 70 | + public byte[] Version { get; set; } |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +或者在 `Fluent API` 中 |
| 75 | + |
| 76 | +```csharp |
| 77 | +protected override void OnModelCreating(ModelBuilder modelBuilder) |
| 78 | +{ |
| 79 | + modelBuilder.Entity<Apartment>() |
| 80 | + .Property<byte[]>("Version") |
| 81 | + .IsRowVersion(); |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +这样在调用 `SaveSchanges` 方法的时候,会抛出 `DbUpdateConcurrencyException` 异常 |
| 86 | + |
| 87 | +2、[于 Moq 中的 SponsorLink 作者的观点](https://codecodeship.com/blog/2023-09-07-daniel-cazzulino) |
| 88 | + |
| 89 | + |
| 90 | + |
| 91 | +前一阵子 `Moq` 库的 `SponserLink` 的事情在 `.NET` 社区掀起了巨大的讨论。在事件平息之后,作者采访了 `Moq` 的作者聊了一下这件事的来龙去脉,包含了下面的话题 |
| 92 | +- 作者对开源认识 |
| 93 | +- 使用 `SponerLink` 的效果 |
| 94 | +- 事件发生后 `Moq` 的现状 |
| 95 | +- 对后续的规划和展望 |
| 96 | + |
| 97 | + |
| 98 | +3、[Weak Reference](https://steven-giesel.com/blogPost/675b75fc-2c1b-43da-9ff8-42962ca8159b) |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | + |
| 103 | +C# 作为一个托管的编程语言,也就是说我们并不需要关心内存的问题。但是有很多情况下会导致内存泄漏,比如事件的的委托。 |
| 104 | + |
| 105 | +```csharp |
| 106 | +public class Publisher |
| 107 | +{ |
| 108 | + public event Action<string> OnChange = delegate { }; |
| 109 | + public void Raise(){ |
| 110 | + OnChange("hello world"); |
| 111 | + } |
| 112 | + public string Label => "Hi"; |
| 113 | +} |
| 114 | + |
| 115 | +public class Subscriper : IDisposable |
| 116 | +{ |
| 117 | + private bool disposed = false; |
| 118 | + public Subscriper() { |
| 119 | + Console.WriteLine("ctor"); |
| 120 | + } |
| 121 | + ~Subscriper() { |
| 122 | + Console.WriteLine("descrt"); |
| 123 | + Dispose(false); |
| 124 | + } |
| 125 | + public void Dispose(){ |
| 126 | + Console.WriteLine("dispose"); |
| 127 | + Dispose(true); |
| 128 | + GC.SuppressFinalize(this); |
| 129 | + } |
| 130 | + |
| 131 | + protected virtual void Dispose(bool disposing) |
| 132 | + { |
| 133 | + if (!disposed) { |
| 134 | + disposed = true; |
| 135 | + } |
| 136 | + } |
| 137 | + public void WriteLine(string s){ |
| 138 | + Console.WriteLine(s); |
| 139 | + } |
| 140 | +} |
| 141 | +``` |
| 142 | +比如这里定义了一个 `Puber` 和一个 `Suber` 类,那么在应用程序中 |
| 143 | + |
| 144 | +```csharp |
| 145 | +Subscriper sub; |
| 146 | +Publisher publisher = new Publisher(); |
| 147 | + |
| 148 | +for (int i = 0; i < 20; i ++) |
| 149 | +{ |
| 150 | + sub = new Subscriper(); |
| 151 | + // publisher.OnChange += sub.WriteLine; |
| 152 | +} |
| 153 | +Console.ReadKey(); |
| 154 | +GC.Collect(); |
| 155 | +GC.WaitForPendingFinalizers(); |
| 156 | +Console.WriteLine(); |
| 157 | +Console.WriteLine(publisher.Label); |
| 158 | +``` |
| 159 | + |
| 160 | +当调用 `GC.Collect` 之后,`~Subscriper` 方法就会被多次执行,因为被 GC 给回收。但是如果我们将 `publisher.OnChange += sub.WriteLine;` 恢复,所有的 `~Subscriper` 都不会执行,因为从 `GC` 角度来看,它注册了一个 `Puber` 的事件。这样导致了内存泄露。 |
| 161 | + |
| 162 | +在 `C#` 中有一个叫做 `WeakReference` 类型,它能够跳过在 `GC` 对标记阶段。该类型有两个重要属性 `IsAlive` 表明引用的对象是否活着,`Target` 能够获得该对象。 |
| 163 | + |
| 164 | +```csharp |
| 165 | +public class WeakEvent |
| 166 | +{ |
| 167 | + private readonly List<WeakReference> _listeners = new List<WeakReference>(); |
| 168 | + |
| 169 | + public void AddListener(Action<string> handler) |
| 170 | + { |
| 171 | + _listeners.Add(new WeakReference(handler)); |
| 172 | + } |
| 173 | + |
| 174 | + public void Raise() |
| 175 | + { |
| 176 | + for(int i = _listeners.Count - 1; i >= 0; i--) |
| 177 | + { |
| 178 | + WeakReference wr = _listeners[i]; |
| 179 | + if (wr != null) |
| 180 | + { |
| 181 | + if (wr.IsAlive) |
| 182 | + { |
| 183 | + ((Action<string>)wr.Target)("hell world"); |
| 184 | + } |
| 185 | + else |
| 186 | + { |
| 187 | + _listeners.RemoveAt(i); |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | +} |
| 193 | + |
| 194 | +public class Publisher |
| 195 | +{ |
| 196 | + private readonly WeakEvent _event = new WeakEvent(); |
| 197 | + public void Raise(){ |
| 198 | + _event.Raise(); |
| 199 | + } |
| 200 | + |
| 201 | + public void Register(Action<string> handler){ |
| 202 | + _event.AddListener(handler); |
| 203 | + } |
| 204 | + public string Label => "Hi"; |
| 205 | +} |
| 206 | + |
| 207 | + |
| 208 | +public class Subscriper : IDisposable |
| 209 | +{ |
| 210 | + private bool disposed = false; |
| 211 | + public Subscriper(){ |
| 212 | + Console.WriteLine("ctor"); |
| 213 | + } |
| 214 | + |
| 215 | + ~Subscriper(){ |
| 216 | + Console.WriteLine("descrt"); |
| 217 | + Dispose(false); |
| 218 | + } |
| 219 | + |
| 220 | + public void Dispose(){ |
| 221 | + Console.WriteLine("dispose"); |
| 222 | + Dispose(true); |
| 223 | + GC.SuppressFinalize(this); |
| 224 | + } |
| 225 | + |
| 226 | + protected virtual void Dispose(bool disposing) |
| 227 | + { |
| 228 | + if (!disposed) |
| 229 | + { |
| 230 | + if (disposing) |
| 231 | + { |
| 232 | + // Dispose managed resources here |
| 233 | + } |
| 234 | + // Dispose unmanaged resources here |
| 235 | + disposed = true; |
| 236 | + } |
| 237 | + } |
| 238 | + |
| 239 | + public void WriteLine(string s) |
| 240 | + { |
| 241 | + Console.WriteLine(s); |
| 242 | + } |
| 243 | +} |
| 244 | +``` |
| 245 | + |
| 246 | +这时候,我们在调用下面这段代码的时候,由于 `sub` 的 `WriteLine` 注册对象是 `WeakReference`, 那么 GC 就会将他们全部回收。 |
| 247 | + |
| 248 | +```csharp |
| 249 | +Subscriper sub; |
| 250 | +Publisher publisher = new Publisher(); |
| 251 | + |
| 252 | +for (int i = 0; i < 20; i ++) |
| 253 | +{ |
| 254 | + sub = new Subscriper(); |
| 255 | + publisher.OnChange += sub.WriteLine; |
| 256 | +} |
| 257 | +Console.ReadKey(); |
| 258 | +GC.Collect(); |
| 259 | +GC.WaitForPendingFinalizers(); |
| 260 | +Console.WriteLine(); |
| 261 | +Console.WriteLine(publisher.Label); |
| 262 | +``` |
| 263 | + |
| 264 | +4、[.NET 8 Performance 提高](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/) |
| 265 | + |
| 266 | + |
| 267 | + |
| 268 | +一年一度的 `.NET` 形成提升的文章又来了,今年轮到 `.NET 8`。这是一篇接近 20 页的文章,如果你有时间,可以快速浏览一下。 |
| 269 | + |
| 270 | +5、[VS for Mac 退役的思考](https://www.youtube.com/watch?v=M7V9ZzxPtVc&ab_channel=IAmTimCorey) |
| 271 | + |
| 272 | + |
| 273 | + |
| 274 | +前一阵子微软宣布明年将不再支持 `Visual Studio for Mac`, 当时引起了巨大的争论,现在作者分享了它对这件事情的看法 |
| 275 | +- 为什么微软创建 `.NET`? 答案就是挣钱,不管是买授权,还是和其他产品绑定,或者做咨询甚至是获取声誉,都是为了挣钱。 |
| 276 | +- `Visual Studio` 并不是一个天生跨平台的软件,有很多 windows 上的依赖。 |
| 277 | +- `VS Code` 是在 Linux 和 Mac 上很好的选项 |
| 278 | +- 这个决定看上去太着急了,尤其对于 MAUI 的发展 |
| 279 | +- 微软需要更多的 `Why` |
| 280 | + |
| 281 | + |
| 282 | +6、[迁移 ASP.NET 到 ASP.NET Core](https://www.jimmybogard.com/tales-from-the-net-migration-trenches/) |
| 283 | + |
| 284 | + |
| 285 | + |
| 286 | +这是一些列博客,作者介绍了如何将 `ASP.NET Framework` 的应用程序迁移到 `ASP.NET Core`, 作者采取了一种渐进式的迁移方式。通过一个空的 `ASP.NET Core` 的应用程序,然后将请求转发原本的应用程序,然后逐步迁移其他 Controller. |
| 287 | + |
| 288 | +7、[EF Core 中不同的 load 方式](https://blog.jetbrains.com/dotnet/2023/09/21/eager-lazy-and-explicit-loading-with-entity-framework-core/) |
| 289 | + |
| 290 | +在数据库中,我们通过外键的方式将不同的表链接起来,在 EF Core 中,我们可以通过这种方式来多表查询,主要有三种方式 |
| 291 | + |
| 292 | +1. Eager loading |
| 293 | + |
| 294 | +这种方式是通过 `Include` 语句来包含子表 |
| 295 | + |
| 296 | +```csharp |
| 297 | +var invoices = db.Invoices |
| 298 | + .Include(invoice => invoice.InvoiceLines) |
| 299 | + .ToList(); |
| 300 | +// All invoices are already loaded... |
| 301 | +foreach (var invoice in invoices) |
| 302 | +{ |
| 303 | + // ...including all their Invoice lines |
| 304 | + foreach (var invoiceLine in invoice.InvoiceLines) |
| 305 | + { |
| 306 | + // ... |
| 307 | + } |
| 308 | +} |
| 309 | +``` |
| 310 | + |
| 311 | + |
| 312 | +2. Lazy loading |
| 313 | +这种方式只有在进行对子表属性访问的时候,才会发送查询请求 |
| 314 | + |
| 315 | +```csharp |
| 316 | +var invoices = db.Invoices |
| 317 | + .ToList(); |
| 318 | + |
| 319 | +// All invoices are already loaded... |
| 320 | +foreach (var invoice in invoices) |
| 321 | +{ |
| 322 | + // ...invoice lines are queried when accessed... |
| 323 | + foreach (var invoiceLine in invoice.InvoiceLines) |
| 324 | + { |
| 325 | + // ...the related product is also queried when accessed |
| 326 | + var product = invoiceLine.Product; |
| 327 | + |
| 328 | + // ... |
| 329 | + } |
| 330 | +} |
| 331 | +``` |
| 332 | + |
| 333 | + |
| 334 | +3. Explicit loading |
| 335 | + |
| 336 | + |
| 337 | +通过 `Load()` 方法显示的获取相应的子表 |
| 338 | + |
| 339 | +```csharp |
| 340 | +var invoices = db.Invoices |
| 341 | + .ToList(); |
| 342 | + |
| 343 | +// All invoices are already loaded... |
| 344 | +foreach (var invoice in invoices) |
| 345 | +{ |
| 346 | + // ...but you'll have to explicitly load invoice lines when they are needed |
| 347 | + db.Entry(invoice).Collection(p => p.InvoiceLines).Load(); |
| 348 | + |
| 349 | + foreach (var invoiceLine in invoice.InvoiceLines) |
| 350 | + { |
| 351 | + // ... |
| 352 | + } |
| 353 | +} |
| 354 | +``` |
0 commit comments