
聊.NET Core的时候我总爱先问一句你分得清.NET Framework、.NET Core和.NET 8的区别吗很多人代码写了不少new一个项目出来跑得飞快但这个问题一上来就卡壳。这不是我较真而是这套关系没理清后面接触依赖注入、中间件、跨平台部署时到处都感觉隔了一层纸。这篇文章就把.NET Core这条技术路线的根基摸清楚。不管你是刚转行进来、还有点怕命令行的新人还是写了好几年.NET Framework想迁移的老开发或是把.NET Core当作第二语言在学的后端工程师下面这些内容应该能补齐你的认知拼图而且重点不是堆术语是讲清楚每个概念到底解决什么问题。1. 先搞清一件事.NET Core和.NET Framework到底什么关系1.1 当年为什么非要另起炉灶先回忆一下背景。.NET Framework从2002年出生绑定的就是Windows这套生态版本也和系统版本纠缠得特别深——你想在Windows 7上用新版的.NET Framework往往根本装不上去想升级还得看系统脸色导致大量生产环境长期停留在某一个老版本上。后来情况变了。服务器端开始大量用Linux移动端和云原生的需求越来越多。老框架这种“锁死在Windows上”的玩法让很多人干着急。2014年官方宣布要做一个开源、跨平台的.NET2016年. .NET Core 1.0正式发布。它的目标非常明确能在Windows、Linux、macOS上跑能容器化部署能按照组件灵活裁剪而不是拖着整个框架到处走。正是这个“另起炉灶”的决定才让今天你在任何一台Linux服务器上都能轻松跑.NET服务。没有这一步后面所有关于部署、高并发、微服务的话题都无从谈起。1.2 三代框架的定位差异很多新手看到.NET Framework、.NET Core、.NET 6/7/8这些名词就头大我直接用一张表把它们的定位说清框架平台支持开源方式版本策略今天的定位.NET Framework仅Windows部分开源跟Windows版本绑定迭代慢只维护不再加新功能.NET Core 1.0 ~ 3.1Windows/Linux/macOS完全开源快速迭代两年大版本技术路线奠基已被统一.NET取代.NET 5及以上Windows/Linux/macOS完全开源LTS版本两年一更STS一年一更当前主流新项目首选这里有个容易误解的点.NET Core到3.1之后就“消失”了但它不是死了而是换了个名字继续往前走。.NET 5、.NET 6、.NET 8本质上就是.NET Core的延续官方把“Core”从名字里去掉是因为从.NET 5开始它不仅包含跨平台运行时还把原来只在.NET Framework里提供的API也统一了进来形成一套真正唯一的.NET平台。所以你现在搜“.NET Core基础”搜出来的教程哪怕写的是.NET 6或.NET 8底层那套东西——CLR、BCL、ASP.NET Core、EF Core——和.NET Core一脉相承学会一点整套都通。1.3 新项目该怎么选型我的建议一向很直接除非你被强制要求维护老系统的代码或者客户明确指定只能在Windows上用.NET Framework否则新项目一律用.NET 8或更新的LTS版本。原因有三点。第一.NET Framework 4.8是最后的绝唱官方只做安全维护不再增加任何新特性。第二跨平台能力是现实刚需——哪怕你今天只在Windows上部署明天客户说要在Linux服务器上跑容器你也不至于手忙脚乱。第三新版本的性能改进非常大。从.NET Core 3.1到.NET 6再到.NET 8光是我实测的Web API吞吐量就有明显提升这套性能红利是老框架完全吃不到的。至于.NET Standard是什么角色它本质上是一个API兼容约定。比如你要写一个类库给多个.NET版本共用就定义一套标准让不同实现都遵守。现在主流框架都统一到了.NET 8类库直接写net8.0就行.NET Standard的必要性已经大幅下降但你在老项目里还会见到它知道它是什么就好。2. 环境准备SDK、IDE和那几行配置命令2.1 装SDK时最容易搞混的Runtime和SDK我第一次接触.NET Core时在官网下载页面愣是看了半天。页面上有两个大按钮一个是“Runtime”一个是“SDK”。很多新手顺手就点了Runtime装完发现终端里敲dotnet命令直接提示“不是内部或外部命令”然后开始怀疑人生。简单说Runtime是运行现成程序用的SDK是开发用的。SDK里包含了Runtime外加编译器、dotnet命令行工具、模板项目等一堆东西。你装SDK就够了不用再单独装Runtime。这就像你买了一套完整的工具箱里面已经带了一把螺丝刀你还额外去买一把纯属浪费。安装完成后在终端里执行dotnet --version和dotnet --info验证一下。dotnet --version dotnet --infodotnet --info会显示你本机装了哪些.NET版本、Runtime有哪些、SDK路径在哪。我强烈建议你养成看--info的习惯因为后面很多莫名其妙的问题都是“同时装了多个版本然后版本冲突”造成的。另外提醒一句官方下载页面会把LTS版本标出来优先选LTS。短周期版本虽然能提前体验新特性但对于刚入门的学习者LTS的社区资料更丰富、坑也少很多。2.2 用命令行创建第一个项目创建项目不要依赖IDE的可视化按钮从第一课开始就习惯用命令行。命令行输出更透明连创建的先后顺序都看得一清二楚出问题时一眼就能看出来。打开终端进入你想放项目的目录执行dotnet new console -n HelloWorld cd HelloWorld这会在HelloWorld目录下生成一个最小的控制台项目。看下目录里的文件核心是Program.cs和HelloWorld.csproj另外还有obj和bin两个文件夹——很多新人看到这两个文件夹就发慌以为是项目错误生成的垃圾。bin是编译输出目录最终跑的dll和exe都在里面obj是编译过程的中间产物。这两个目录不需要手动处理也不用提交到版本控制把它们写进.gitignore就行。接着编辑Program.cs默认内容长这样Console.WriteLine(Hello, World!);然后执行dotnet run你会看到控制台打印出“Hello, World!”。如果你先执行dotnet build再执行dotnet run会发现dotnet run本身就会隐式执行编译——所以日常开发直接用dotnet run就够了。2.3 csproj项目文件里藏着哪些门道老一点的开发者应该还记得旧版.NET Framework的项目文件又长又难懂什么AssemblyInfo、ProjectReference、条件编译符号手动编辑一份csproj能让人崩溃。.NET Core从设计上就重写了csproj格式现在叫“SDK风格项目文件”内容非常精简。打开HelloWorld.csprojProject SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeExe/OutputType TargetFrameworknet8.0/TargetFramework ImplicitUsingsenable/ImplicitUsings Nullableenable/Nullable /PropertyGroup /Project你需要关心的就几行。TargetFramework决定了你的项目跑在哪个版本的.NET上这里写net8.0就表示目标框架是.NET 8。ImplicitUsings开启后很多常用命名空间System、System.Collections.Generic、System.Linq等系统自动引入代码里不用再写一堆using。Nullable开启后编译器会强制你考虑空值问题这是现代C#代码质量的根基建议开启。往项目里加NuGet引用也很简单dotnet add package Newtonsoft.Json执行后csproj里会自动多一行PackageReference。这个文件现在完全可以直接手写比在IDE里一顿点来点去高效得多。3. 项目落地前必须理解的五个基础概念3.1 托管代码与GC.NET是一个“托管运行时”环境。你写的C#代码并不会直接编译成机器码而是先编译成一种中间语言IL等程序运行起来再由运行时通过JIT即时编译翻译成当前CPU能执行的机器码。这层中间语言就是托管代码的核心它让同一套代码能在Windows、Linux、macOS上跑因为翻译工作交给了对应平台上的运行时。托管运行时另一大贡献是GC垃圾回收。C和C里面你亲手new出来的内存必须亲手释放忘一次就内存泄漏多释放一次就崩溃。.NET的GC会定期扫描托管堆找出那些不再被引用的对象并自动回收。这让开发者的心智负担大幅降低。但GC不是不管内存的借口。比如你打开了一个文件流、一个数据库连接这些属于非托管资源GC管不到那么及时必须手工释放。所以IDisposable接口和using语句是.NET开发者必须掌握的日常技能。using (var stream new FileStream(log.txt, FileMode.Open)) { // 读完自动释放 }一句话总结GC帮你管托管内存非托管资源还得你亲自管。3.2 程序集、命名空间和类库每次编译生成的dll或exe在.NET的世界里叫“程序集”。一个项目通常编译成一个程序集它是代码打包和版本控制的最小单元。命名空间则是一层逻辑组织。比如System.Collections.Generic表示它下面放着泛型集合类Microsoft.Extensions.DependencyInjection表示它放着依赖注入相关服务。用命名空间把类按功能包起来避免类名冲突——就像文件系统里用目录分文件一样你叫File.cs的文件放在A目录和B目录里互不影响。理解这两者的区别很有用你引用一个项目或者一个dll是为了用里面的公共类型你写using是为了少打几个全限定名。两者不是一回事引用是对“程序集”的依赖using只是语法糖。3.3 基类库BCL不用什么都自己造轮子BCLBase Class Library是.NET平台自带的一套基础类库包含集合、字符串处理、文件IO、正则表达式、JSON、HTTP请求、LINQ等等。它决定了你日常开发最少要“造多少轮子”。比如你想从一个数组里筛选出所有长度大于3的字符串C#写起来是这样的var longWords words.Where(w w.Length 3).ToList();这行代码在 .NET Framework时代就是LINQ带来的在.NET Core里依然是常用操作。你不需要去写一堆for循环和判断逻辑BCL已经替你做好了。了解BCL里有哪个工具、它适用于什么场景远比背几个类的API重要。我自己的习惯是遇到一个需求时先想“BCL里有没有现成的”搜不到再自己写。3.4 NuGet.NET世界的包管理器用任何语言都逃不开包管理。.NET这边用的就是NuGet它和前端领域的npm、Python里的pip是同一类东西。你通过dotnet add package命令把别人写好的类库下载到本地项目项目的csproj里就会记录一个PackageReference这样团队其他人拉下代码后dotnet restore一下就能把包都装回来。企业内部往往会建一套私有NuGet源存放公司内部的基础组件。配置也简单dotnet nuget add source http://你的内部包源/v3/index.json -n InternalSource配上之后dotnet add package就可以从这个源里安装内部组件了。刚才有个朋友问我为什么他的项目在别人电脑上跑不起来一问才知道他直接把bin目录拷给同事。这当然不行——源码里只要有csproj和代码文件拉下来执行dotnet restore加dotnet run所有依赖自动搞定。3.5 目标框架你编译给谁看目标框架这个概念值得单独拿出来说因为它是很多编译错误的源头。TargetFramework里写net8.0表示你的项目引用的API是.NET 8提供的运行时就要求目标机器装有.NET 8运行时。如果你写了一个类库希望它既能被.NET 8项目用也能被老项目用那就可以写多目标TargetFrameworksnet8.0;netstandard2.0/TargetFrameworksnetstandard2.0这个“兼容券”能让类库在更多老平台上跑。但代价是你只能在代码里使用两者交集的那部分API。如果你的类库用到了只有.NET 8才有的API那netstandard2.0那半边就会编译失败。打个比方目标框架就像你手里的优惠券。券上写“全门店通用”适用范围就广但能换的商品也基础的券上写“仅本店可用”适用范围窄换购的东西却高端。你根据项目现实去选就行。4. 从控制台到Web API一个完整项目的演化过程4.1 先把一个最简单的控制台程序拆开看很多人习惯了IDE一键模板根本不知道一个程序是怎么从代码到进程的。我们从最简单的控制台项目开始看清楚它的入口。现在.NET 8的Program.cs默认用的是顶层语句文件里直接写Console.WriteLine就能跑但底层其实还是有一个Main方法作为入口。编译器替你隐藏了。如果你在代码里声明一个args变量就能拿到命令行参数if (args.Length 0) { Console.WriteLine($第一个参数是{args[0]}); }再执行dotnet run -- hello程序就能把“hello”打印出来。理解入口点很重要因为不管是控制台程序、Windows服务还是Web应用本质都是从一个入口启动然后执行一堆初始化逻辑之后进入处理循环。4.2 加一个ASP.NET Core Web API控制台程序只相当于一个“脚本”真正的业务系统要对外提供接口那就得请出ASP.NET Core。创建命令dotnet new webapi -n MyWebApi dotnet run新项目自带的示例接口就能访问了。浏览器打开http://localhost:5000/weatherforecast你会看到一份JSON数据返回。这个模板项目里核心配置文件叫appsettings.json端口、日志、数据库连接字符串等都可以在里面配置。{ Logging: { LogLevel: { Default: Information } }, AllowedHosts: *, ConnectionStrings: { Default: serverlocalhost;databasetest;uidsa;pwd123456 } }读完配置你可以在代码里通过IConfiguration接口访问var connStr builder.Configuration.GetConnectionString(Default);这是.NET Core体系里一个很顺滑的体验配置、依赖注入、日志、中间件在Web项目里全部启动器WebApplicationBuilder统一管理。这也解释了为什么说ASP.NET Core“只是长得像控制台程序”——它的Program.cs本质上就是顺序执行若干注册步骤。4.3 中间件管道请求处理的一串关卡Web项目的Program.cs里有一行接一行的app.UseXxx()这些就是中间件。每个请求从进入到你写的业务代码会依次穿过一个个关卡像流水线一样。看这段典型配置app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();这里的顺序不能乱。UseRouting负责匹配哪个接口处理当前请求UseAuthentication负责确认你是谁UseAuthorization负责确认你有没有权限MapControllers最终把请求转给对应的控制器方法。如果你把UseAuthorization放到UseRouting之前授权中间件无法知道目标接口需要什么权限可能直接放行所有无效请求。再比如想给所有接口加一个耗时日志可以在管道最前面加app.Use(async (context, next) { var sw Stopwatch.StartNew(); await next(); sw.Stop(); Console.WriteLine(${context.Request.Path} 耗时 {sw.ElapsedMilliseconds}ms); });理解了中间件管道ASP.NET Core请求处理机制你就掌握了7成。中间件方法职责顺序误区UseRouting路由匹配放在UseAuthentication之前UseAuthentication身份认证不能晚于UseAuthorizationUseAuthorization授权校验必须在认证之后MapControllers映射到Controller放最后5. 依赖注入.NET Core最被低估的灵魂设计5.1 为什么不用new了在.NET Framework时代我们写服务层时经常犯一个毛病想用哪个服务就直接new一个实例。比如OrderService里需要访问数据库于是便var repo new OrderRepository();。一开始代码少还好项目大了以后这些new出来的依赖散落在各个角落你想换一个实现、加一层日志、做一个单元测试都得拆一堆代码。依赖注入的核心思想是“不要自己找依赖而是让容器把依赖递给你”。用生活场景类比你不需要知道餐厅后厨怎么备菜你只需要告诉服务员你要什么菜服务员会把菜端到你面前。C#里的构造函数注入就是服务员public class OrderService { private readonly IOrderRepository _repository; public OrderService(IOrderRepository repository) { _repository repository; } }OrderService不再负责new仓库而是声明“我需要一个IOrderRepository”。根据这个接口容器在创建OrderService时会自动传入合适的实现。以后想换实现只需要改注册处一行代码解决。5.2 三种生命周期怎么选依赖注入里最让新手懵圈的就是生命周期。注册服务时你需要决定每个服务实例活多久。生命周期行为典型场景注意事项Transient每次请求都是新实例轻量无状态的服务像工具类不要在里面保存共享状态Scoped同一次请求内复用同一个实例EF Core的DbContext相当于每请求一个实例Singleton全局只有一个实例配置管理器、Redis连接池注意线程安全问题比如用EF Core查数据库它的DbContext必须注册为Scoped。因为同一次HTTP请求中多个服务如果共享同一个DbContext事务和状态管理才一致如果注册成Singleton那所有请求都挤同一个对象数据库并发直接炸掉。最容易踩的坑是在Singleton服务里注入Scoped服务。比如一个全局缓存服务想在启动时查一下数据库直接在构造函数里注入DbContext运行后会发现这个DbContext变成了一个“根作用域”下创建的、几乎永不释放的实例连接池也不断增长。正确做法是注入IServiceScopeFactory每次需要时再手动创建一个子作用域public class CacheService { private readonly IServiceScopeFactory _scopeFactory; public CacheService(IServiceScopeFactory scopeFactory) { _scopeFactory scopeFactory; } public void Refresh() { using var scope _scopeFactory.CreateScope(); var db scope.ServiceProvider.GetRequiredServiceAppDbContext(); // 查询并写缓存 } }5.3 一个实际项目里DI的落地写法整体注册入口在Program.csvar builder WebApplication.CreateBuilder(args); builder.Services.AddScopedIOrderRepository, OrderRepository(); builder.Services.AddScopedOrderService(); var app builder.Build();然后OrderController里再注入OrderService链条就打通了。有人可能会问接口只有实现类的时候能不能不写接口可以直接注册类本身也行。但建议保留接口因为单元测试时你需要用Mock对象替换真实实现依赖的是“抽象”这才是依赖注入的本意。还有一个常被踩的坑是有人在代码里到处注入IServiceProvider然后用GetRequiredService去拿服务。这叫服务定位器反模式它让代码隐式依赖容器类型测试和重构都很痛苦。IServiceProvider只应该出现在启动流程和极少数特殊场景正常业务代码一律用构造函数注入。6. 异步编程别让await只是看起来对了6.1 异步到底解决了什么问题很多初学者以为异步就是“更快”其实是搞混了两件事。异步不提高单次操作的速度它解决的是“等待时别占着线程”的问题。想象一个餐厅服务员A点完菜后站在传菜窗口干等10分钟期间其他客人招呼也不理。这种对立式服务效率极低。异步的做法是点完单之后服务员继续去服务别的桌等菜做好了再回来给这一桌上菜。线程就像服务员它不在等待时空转系统就能用更少的线程处理更多的请求。在.NET里网络请求、文件读写、数据库操作都是IO密集型的天然适合异步。拿一个真实接口举例它要查数据库耗时200毫秒这200毫秒里如果线程被同步卡住该线程就完全闲置了改用await之后线程立刻回到线程池去服务别的请求等数据库结果回来再恢复执行。高并发场景下就是这200毫秒的差异决定了系统能不能顶住压力。6.2 async/await的常见误区第一个误区是async方法里没有awaitpublic async Taskint GetValueAsync() { return 42; }编译器会直接警告这个async没有意义方法会同步执行完。你要么去掉async要么在里面真正使用await。第二个误区是滥用async void。事件处理器里可以写async void但普通方法里千万别用——如果异步方法内部抛异常async void方法会把异常抛到一个你无法捕获的同步上下文里轻则程序崩溃重则日志里什么都不留。凡是返回类型能写成Task的一律不写void。第三个误区是同步等待结果。在库代码里用.Result或.Wait()同步阻塞异步方法不但会导致线程被冻住在带同步上下文的UI环境还会死锁。一个HTTP调用你等它的结果返回本来应该把线程交回线程池你偏偏让线程站在那死等那跟你写同步代码有什么区别原则就是从入口到数据库访问整个链条都用async/await不要中途混入同步阻塞。6.3 什么时候用Task.Run什么时候不用新手还容易犯一个错方法里明明有IO操作却非要包一层Task.Runvar data await Task.Run(() httpClient.GetStringAsync(url));这属于脱裤子放气。IO操作本身就是异步的HttpClient.GetStringAsync返回的已经是异步任务不需要再用Task.Run去线程池里开新线程。这样当你await Http时新线程同样会被释放白多一次线程切换。Task.Run的正确使用场景是CPU密集型计算。比如有一批大数据要做复杂计算你想利用多核并行就用Task.Run把任务拆给多个线程。一句话区分IO密集用async/await加原生的异步操作CPU密集才考虑Task.Run或Parallel。把这条原则想清楚你的代码才不会出现“看起来异步、实际更慢”的情况。7. 我在实战中踩过的坑和排查思路7.1 版本不匹配引发的连锁故障有次承接一个老项目的升级任务技术栈从.NET Core 3.1迁到.NET 6。我直接改了TargetFramework然后dotnet build屏幕上瞬间刷刷刷出来几百条错误场面特别壮观。先冷静下来分类。一种错误码是NU1202结尾的意思是某个NuGet包不兼容当前的目标框架。查到原因后发现很多老包发布时的目标框架是netstandard2.0或netcoreapp3.1和net6.0不兼容。解决办法不是硬写net6.0去让编译器抓瞎而是把这些包升级到兼容net6.0的更高版本。另一种错误码是NU1107某个包依赖了另一个包的两个不同版本冲突了。这时不要手动瞎改包版本用命令更靠谱dotnet list package --vulnerable dotnet list package --outdated--vulnerable能查出有安全漏洞的版本--outdated能看出哪些包有新版。然后按“依赖树”一层层升级而不是把所有包都升到最新——全部升最新有时候反而引入其它兼容问题。排查这类问题有个不变的主线看错误码把它当作线索检查目标框架逐项升级依赖每过一步就编译一次。不要一个晚上全包升完再编译那样万一出错你根本不知道是哪一步捅了篓子。7.2 HttpClient对象引发的连接资源泄漏另一个特别常见的坑是HttpClient用多了导致连接耗尽。我有一次写一个定时同步服务往外部某个内部系统发HTTP请求跑了几天后发现请求越来越慢最后直接报连接超时。HttpClient这个类虽然实现了IDisposable但底层的socket连接并不会因为你Dispose就立刻释放它会进入TIME_WAIT状态。如果你每次请求都new HttpClient旧连接还没来得及复用就被丢弃新请求又创建新连接长期跑下来socket越积越多最终把端口和连接池都耗光。正确做法不是“用完就释放”而是用IHttpClientFactory管理生命周期builder.Services.AddHttpClient(MyClient, client { client.Timeout TimeSpan.FromSeconds(30); });然后在需要的地方通过工厂创建客户端public class WeatherService { private readonly IHttpClientFactory _httpClientFactory; public WeatherService(IHttpClientFactory httpClientFactory) { _httpClientFactory httpClientFactory; } public async Taskstring GetAsync(string url) { var client _httpClientFactory.CreateClient(MyClient); return await client.GetStringAsync(url); } }IHttpClientFactory会帮你做连接池复用和超时释放DNS变更也处理得好。排查这个问题的链路其实不复杂先看异常信息是不是“连接被强制关闭”或“超时”再用网络命令看本机连接数发现异常数量远大于正常请求量再去看代码有没有反复new对象。7.3 Linux容器里的文件路径与大小写问题.NET Core带来的跨平台能力不是白给的Windows上跑得好好的程序一到Linux容器里就各种翻车。我记得有一次某个内部部署同步服务在容器里崩溃日志提示找不到某个配置文件。根本原因有两个。第一是文件路径分隔符。Windows用的\Linux用/我在代码里直接用conf\\app.json写死了换到Linux自然找不到。正确的做法是用Path.Combinevar configPath Path.Combine(env.ContentRootPath, conf, app.json);Path.Combine会根据当前系统自动选择分隔符这条经验适用所有跨平台开发。第二是大小写敏感。Windows的文件系统不区分大小写而Linux区分。之前我在某个事件处理里写了一个appsettings.json代码里却读的是AppSettings.JsonWindows上测试一切正常放到Linux容器里直接FileNotFoundException。排查这类问题的时候第一不要怀疑代码逻辑第二第一反应就问自己是不是路径分隔符写死了是不是大小写不一致用Path.Combine之后这两个坑至少避掉一个。8. 从基础到进阶学了这些下一步干什么8.1 建议的学习路径基础概念掌握了以后最怕的就是陷入“学完感觉都会动手什么都不会”的状态。我的建议是抛一个真实需求逼自己去做比如“做一个待办事项API”。第一步用ASP.NET Core Web API搭建接口实现增删改查。第二步引入EF Core连上SQLite把内存数据落到数据库。第三步加上DTO和Data Annotation做参数校验。第四步把日志接到控制台和文件加一个全局异常处理中间件。第五步部署到Linux服务器上的Docker容器里完成整个开发闭环。走完这一轮你对.NET Core的基础就有了肌肉记忆。后面再接触微服务、消息队列、Redis缓存、分布式事务之类的进阶内容才会有一个稳固的根基可以挂靠。8.2 值得长期关注的资料源文档优先看官方技术文档内容虽然啰嗦但权威性和更新速度最好。遇到具体问题时Stack Overflow上的.NET相关回答质量很高搜索时带上.NET版本号能避开大量过时答案。GitHub上有不少架构比较规整的开源项目阅读它们的代码是很好的学习方法。看的时候不要只看代码本身要看目录结构怎么分、依赖注入怎么组织、异常怎么处理、配置怎么拆。我常说好的开源项目就是最好的“免费导师”。另外一个容易被忽略的资源是各云厂商的官方文档中心。那里面有很多落地部署、负载均衡、监控告警的实战案例比单纯看框架源码更贴近生产环境。8.3 心态与学习方法最后说点我个人经验。学.NET Core最大的敌人不是难度而是分散——想一口气把依赖注入、EF Core、中间件、异步、配置系统、日志、身份认证全学完反而每个都学不深。我自己后来定了一条规则一个阶段只聚焦一个主题用真实项目去验证。比如这周只研究依赖注入那就把现有项目里所有“new出来的服务”改成注入方式顺便加深对生命周期的理解。弄透了再换下一个。别贪多别求快底层概念扎实了后续框架层面的变化对你来说都会变得不那么难。还有一点小技巧多看看你所用工具生成的默认代码。dotnet new webapi生成的启动代码里藏着依赖注入、中间件、配置加载所有的最佳实践。你不需要额外找教程把默认代码里每一行都弄明白比看十篇二手教程都管用。