多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

C# Web开发实战:从ASP.NET Core入门到企业级应用

C# Web开发实战:从ASP.NET Core入门到企业级应用 很多人听到C#第一反应还是那个微软出品的、写Windows桌面程序的编程语言。我是真的见过有同学在群里问C# Web开发C#能做网页吗不但能做而且现在企业级Web开发的主流技术栈里C#和ASP.NET Core是相当能打的一支。我过去几年用C#做过API服务、后台管理系统、还做过一套给现场人员用的数据采集前端接口回头再看这条路其实特别好走只是入门的时候信息太分散容易走弯路。这篇文章就当成一个老开发者的实操笔记来看吧。适合谁刚把C#基础语法学完、正盯着Web方向犹豫的人也适合前端开发想学后端、被Node/Java整得头皮发麻的同学。我会先从最核心的概念讲起然后带着你一步步把项目建起来、把页面跑出来最后把踩过的坑都摆出来。1. 先搞明白C# Web 开发到底在做什么1.1 你写的每一行代码最后都跑在一个 Web 服务器里很多初学C# Web的人最大的认知坎在这里C#代码不会变成浏览器里那些花花绿绿的页面它始终待在服务端。你打开浏览器地址栏输入网址本质上是给某个Web服务器发了一个HTTP请求。服务器拿到请求后找到对应的C#类执行里面的逻辑再把结果以HTML、JSON、图片、PDF文件等形式返还给浏览器。这就是“请求-处理-响应”的完整回路。我常用一个餐厅的类比来帮助新手理解。浏览器是客人Web服务器是厨房C#代码是厨师。客人点菜是请求厨师看菜单、切菜、下锅是业务逻辑端出来的菜是响应。至于HTML和JavaScript那更像菜盘子上的装饰摆盘好看是好看但真正的味道来自后厨。你用ASP.NET Core建一个项目框架会自带一个轻量级的、完全免费的Web服务器叫Kestrel它负责监听本地端口接住浏览器发来的请求。所以你不需要像老一代开发那样必须再装一套IIS或Apache才能把网站跑起来。这个“跑在服务器上”的意识一旦建立后面很多概念都是顺水推舟。比如为什么C# Web项目要处理线程并发因为服务器同一时间会接住成千上万个客人点单每桌菜不能上串。为什么要有依赖注入因为后厨的调料不能靠每道菜自己出去买得有人统一把盐和酱油备好送进来。后文我们展开这些具体手段时你心里始终挂着这条链路就不会晕。1.2 选对框架ASP.NET Core 为什么是现在的主流C#做Web不是一个新鲜事。早年间有ASP.NET Web Forms想帮程序员把Web开发做得像WinForm一样拖控件后来出了ASP.NET MVC去掉了控件那层沉重封装再后来微软痛下决心把这些技术统一重写成ASP.NET Core跨平台、模块化、高性能。现在新项目再用老Web Forms启动我只会劝你慎重因为微软早已把重心放在Core上生态、学习资料、云部署方案都是以Core为准。为什么要选ASP.NET Core而不是继续学老框架第一是跨平台。同一个项目可以跑在Windows、Linux、macOS上丢到Docker容器里也自然。第二是性能。Kestrel在官方跑分中表现相当亮眼加上.NET本身的编译优化应对中等规模的商业站点完全没问题。第三是生态统一。Web API、MVC页面、Razor Pages、Blazor、SignalR实时通信、gRPC全都在一个框架体系里会了一个之后扩展到其他功能学习曲线很平缓。企业级Web开发尤其看重可持续维护。ASP.NET Core从封装里暴露了更清晰的控制权中间件管道可以按需加入、自定义顺序。你现在学会的这套逻辑过五年也不会过时。所以如果你让我做选型新项目99%都会落在ASP.NET Core上版本优先选当前最新的LTS版本。1.3 C# 基础不是 HTML/CSS但你必须会这些有人可能会问我C#刚学会循环、数组、方法能不能直接学Web能但效率会很差。Web开发用到C#的地方和纯控制台程序有重叠但方向偏向“组织业务逻辑”。你至少得熟练这些内容类与接口、泛型与集合、LINQ、异步编程、字符串处理、异常处理。如果连类库怎么引用、NuGet包怎么安装都没概念后面写项目会非常痛苦。举个真实例子。我说要截取一个字符串里的某个业务编号新手可能写一个for循环一个字符一个字符找顺手的人直接Substring配合IndexOf十秒钟解决。这个不是炫技而是你越早熟悉基础API后边写复杂业务的时候越能把精力放到产品逻辑上而不是语言本身。同时也要摆正心态HTML、CSS、JavaScript是另一套技术不是C#的一部分。纯后端程序员可以不太会设计但至少要读得懂页面结构知道form怎么提交、JSON长什么样、一段fetch代码在干什么。因为Web开发最经常做的事就是让C#后端页面和前端页面协作。你可以不写漂亮样式但前后端之间如何传数据这件事躲不开。2. 动手前的准备开发环境、项目结构与依赖库2.1 从零搭一个 ASP.NET Core Web 项目我默认你已经装了.NET SDK或者Visual Studio。但不管用哪个最稳、最能看清皮肉的命令行方式永远是这几个命令dotnet new webapp -n MyWebApp cd MyWebApp dotnet run执行完框架会启动一个Kestrel服务器默认监听某个本地端口浏览器会自动弹出模板页面。看到那个页面你的第一个ASP.NET Core项目就跑起来了。这里要强调一句-n MyWebApp是指定项目名称你也可以取别的名字比如-n TodoWeb。创建出来的项目结构看起来有点多但核心就几样Program.cs整个应用的入口。框架在启动时执行这里的代码把服务注册好、把中间件管道搭好。Pages或Controllers存放页面或接口逻辑。Razor Pages模板默认用Pages目录。wwwroot静态文件的家。CSS、JS、图片都放这里。appsettings.json配置文件的默认位置连接字符串、日志级别都在这里。.csproj项目文件记录依赖包和目标框架。很多新手一看到这么多文件夹就想背没必要。你只需要知道“启动看Program.cs业务代码看Pages/Controllers配置看appsettings.json”剩下的遇到再查。随着你写的东西变多这些文件的位置自然就长在你脑子里。2.2 类库与依赖注入组织代码的最小单元刚写Web项目时最容易犯的错是把所有代码都堆在同一个项目里。一开始Controller只有几个文件还挺爽。等到业务上来了几千行代码堆在里面找一个方法翻半天改一处地方还要小心翼翼怕碰坏别处。这时候你就需要有意识地把代码拆成类库。类库简单讲就是另一个不直接启动、专门放可复用代码的项目。你的解决方案里可以这样规划MyWebApp.Core放领域实体和接口MyWebApp.Infrastructure放数据库访问的实现MyWebApp.Web是真正启动的Web项目。Web项目引用前两个类库通过NuGet也可以引用第三方库。每次拆类库你都在给项目留未来扩展的余地这是C#高级编程实践里很重要的一条。类库只是代码的家真正把各个部分串起来的是依赖注入。ASP.NET Core从设计之初就把依赖注入内置了。你不需要到处new ProductService()只要在Program.cs里声明一次var builder WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddScopedIProductService, ProductService(); var app builder.Build();之后在Controller的构造函数里声明IProductService框架会在请求到来的时候自动把ProductService实例送进来。为什么非要用这套流程最大的好处是解耦和测试。你写单元测试时可以很轻松伪造一个假的IProductService塞进去不需要依赖真实数据库或真实文件系统。这个技能在工作环境里几乎天天用。2.3 本地调试与免费Web服务器选择本地调试的时候你现在遇到的服务器叫Kestrel是ASP.NET Core自带的跨平台Web服务器完全免费开发时用起来非常顺手。不用装Apache不用安装配置IIS直接dotnet run就是一台可用的HTTP服务。不过要泼一盆冷水Kestrel虽然免费又方便但它只适合开发环境生产环境通常需要给它前面加一层反向代理。反代是干什么的可以把公网的HTTPS流量终结在Nginx或IIS上再转发给后面Kestrel这样Kestrel只用处理本地明文HTTP安全策略和证书管理都集中在入口处。Windows上常用IIS作为反向代理Linux上常用Nginx。你要是跑一个个人项目或内部小系统用Kestrel裸跑也不是不能用但必须把HTTPS、防火墙、端口访问权限都处理好。后面第5章我会专门讲安全这里先记住本地调试用Kestrel零成本生产部署永远先想清楚反向代理。3. 从入门到实践完整做一个小型Web应用3.1 定义数据模型和使用EF Core干讲理论容易困我们用一个小项目把链路走通。假设现在要做一个待办事项后端支持新增、查询、完成、删除。第一步是定义数据模型。在类库里写一个TodoItem类public class TodoItem { public int Id { get; set; } public string Title { get; set; } string.Empty; public bool IsDone { get; set; } public DateTime CreatedAt { get; set; } DateTime.Now; }然后引入EF Core这是微软的ORM框架能让我们用C#对象去操作数据库不用手写SQL。先安装对应数据库的包比如开发期用SQLite最省事dotnet add package Microsoft.EntityFrameworkCore.Sqlite接着定义一个AppDbContext表示数据库的上下文public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetTodoItem TodoItems { get; set; } null!; }最后在Program.cs里注册它builder.Services.AddDbContextAppDbContext(options options.UseSqlite(builder.Configuration.GetConnectionString(Default)));连接字符串放在appsettings.json里。这一步之后数据库就像一张无形的表随时可以通过DbContext来读写。你不需要自己维护数据库连接打开关闭EF Core会把UoW模式做好。3.2 后端API开发Controller与路由现在要让前端和外部系统能操作这些待办事项我们做一个标准的REST API。新建一个TodoController继承ControllerBase标上[ApiController]和[Route(api/todo)]。[ApiController] [Route(api/todo)] public class TodoController : ControllerBase { private readonly AppDbContext _db; public TodoController(AppDbContext db) { _db db; } [HttpGet] public async TaskActionResultListTodoItem GetAll() { return await _db.TodoItems.ToListAsync(); } [HttpPost] public async TaskActionResultTodoItem Create(TodoItem item) { _db.TodoItems.Add(item); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetAll), new { id item.Id }, item); } }这里出现了几个关键概念。[HttpGet]和[HttpPost]指定HTTP方法FromBody绑定请求体里的JSON数据。客户端发POST /api/todo带上{title:写文章}请求体就会自动被框架反序列化成TodoItem对象存进数据库。返回的时候用CreatedAtAction规范地返回201状态码。很多刚接触的人不理解“路由”这件事。就拿外卖来说/api/todo就是店铺地址GET是到店取餐POST是下单。后端通过地址和方式决定让哪个Controller里的哪个方法干活。ASP.NET Core的路由规则很灵活但入门阶段你只要会Controller特性路由就够了。3.3 前端页面Razor页面 一点JavaScript如果你的项目不是纯API还要出网页给用户操作可以试试Razor Pages。Razor允许在HTML文件里直接嵌入C#代码语法以开头比如Model.Title。这种模式对新手很友好服务端可以把数据准备好直接渲染成用户看到的HTML不用前后端来回折腾。但我还是要多说一句现在很多团队更习惯前后端分离前端用Vue或React后端只提供API。这种情况下前端页面就写成纯HTML/JS用fetch去调我们上一步写的API。举个最简单的例子在wwwroot下建一个index.html加载时请求待办列表async function loadTodos() { const res await fetch(/api/todo); const todos await res.json(); console.log(todos); } loadTodos();这个看起来很简单但它会帮你理解两个关键技术点一是后端返回的JSON如何变成前端能用的JavaScript对象二是浏览器跨端口、跨域访问时可能被CORS策略拦住。第5章我会讲CORS怎么配置这里先留个印象。选择Razor还是纯前端取决于场景。对后台管理系统、内部工具Razor Pages开发效率极高一个服务端就能搞定对面向用户的营销站、高交互页面分离架构更合适。你初学阶段不用纠结先各跑通一遍感受差别。3.4 Web页面PDF打印与文件导出后台系统做到后面几乎都会被问到一个需求把这段页面打印成PDF。我们分开看最简单的场景是“用户自己在浏览器里打印”。前端按钮直接调window.print()浏览器会弹出自带的打印窗口用户把目标打印机选成“另存为PDF”就行。这一步不需要后端参与非常适合单页报告、工单详情这种场景。为了打印效果好看可以在CSS里单独写一套media print样式把按钮、导航栏、背景色隐藏掉只保留正文。另一个场景是服务端批量生成PDF比如下个月的账单列表用户不可能一个一个打开页面去打印。这时C#后端可以用QuestPDF或DinkToPdf这类库来生成。QuestPDF是免费的开源库API做得很现代用C#代码描述PDF的结构文档也全。我之前用它在几分钟内给一个管理后台做了批量导出合同PDF里面还能插入表格、循环渲染数据行。这里不多展开代码但你可以搜一下QuestPDF的快速上手以后真用到的时候会感谢我今天提到它。理解了PDF再顺带一提文件导出。Excel导出用ClosedXMLCSV导出直接写字符串都是后端非常经典的能力。Web开发的“页面”不只是HTML还包括各种文件响应。后端返回文件时设置好Content-Disposition: attachment浏览器就会自动下载。4. 实战中的坑C# Web 开发常见问题与排查4.1 一切都要先看日志我见过最耽误时间的排查方式就是不看日志、反复刷浏览器。Web应用是黑盒你不知道请求到底有没有进入ControllerSQL执行成什么样只能靠日志还原整个过程。好在ASP.NET Core内置了日志框架你可以在Controller构造函数里注入ILoggerTpublic class TodoController : ControllerBase { private readonly ILoggerTodoController _logger; public TodoController(ILoggerTodoController logger) { _logger logger; } }然后在方法里写_logger.LogInformation(开始查询待办列表数量参数{count}, count);。启动应用时控制台会把这些日志打出来。只要你把关键节点都打上日志排查404、500、数据不对都会快很多。很多新手因为启动时控制台日志太多觉得烦直接关掉结果出了错两眼一抹黑。开发时把日志级别设成Debug上线时设成Information或Warning按需调。不同状态的HTTP响应码也要心里有数。404通常是路由不对或者有没有匹配的Controller500通常是代码炸了400通常是请求参数不对。结合日志和状态码大部分问题不用问别人。4.2 端口、防火墙、部署服务器权限本地调试经常出现的一个问题项目启动后浏览器打不开控制台提示“Unable to listen on port 5000”说明5000端口已经被占用了。解决办法很简单在Properties/launchSettings.json里改一下端口或者启动时指定dotnet run --urls http://localhost:5199部署到云服务器之后问题会换成“外网访问不到”。别急着怀疑程序先检查三个地方进程是不是真的跑了、监听地址是不是0.0.0.0还是只监听localhost、服务器防火墙和云安全组有没有开放对应端口。很多教程会让你用dotnet publish发布后直接跑但发布后的DLL不会自动监听公网端口你需要让Kestrel监听http://0.0.0.0:8080才能被外部访问。在实际生产环境我更推荐把Kestrel藏在Nginx后面Nginx监听80/443再转发给本地Kestrel避免暴露太多内部细节。4.3 异步编程与死锁C# Web开发几乎天天和async打交道因为文件读取、数据库查询这些I/O操作如果同步阻塞会让服务器线程在那里干等白白浪费资源。正确姿势是一路async到底public async TaskActionResultListTodoItem GetAll() { return await _db.TodoItems.ToListAsync(); }关键是不要用.Result或.Wait()去同步等待一个异步方法。我曾经接手过一个老接口里面数据库查询后加了.Result结果白天请求量一上去接口就开始莫名超时。改成await之后准确说把整条调用链上的同步阻塞都去掉吞吐量立刻恢复正常。Web项目里出现死锁的概率不像旧UI编程那么高但阻塞线程池线程导致性能雪崩是实实在在的。还容易踩的坑是忘记处理CancelToken。长时间跑的接口建议接收CancellationToken保证客户端断开后服务端能及时取消操作。这不是入门重点但能让你少背几个线上事故。4.4 数据库连接字符串与敏感信息连接字符串是一个看起来不起眼、但出事特别狠的地方。很多教程直接写着ConnectionStrings:Default: Serverlocalhost;DatabaseMyApp;User Idsa;Password123456;然后学生顺手就提交到Git仓库。在练习项目里无所谓但如果你在公司里也这么干等于把数据库密码公开了。正确的做法是开发环境用用户机密User Secrets部署环境用环境变量或者密钥管理服务。ASP.NET Core的配置系统会自动把环境变量覆盖到appsettings.json上所以你在服务器上设好环境变量ConnectionStrings__Default代码不用改。这里也顺带提醒一句任何密码、令牌、私钥都不要写死在代码里。如果有人从历史提交里翻出你的数据库密码那不只是你的项目完蛋还可能连累整个服务。安全意识和写代码水平一样重要。5. 进阶实践企业级应用需要补的课5.1 认证授权与安全个人练习项目可以不用登录但企业级Web开发几乎不可能绕过认证授权。ASP.NET Core提供了两套主流方案Cookie认证和JWT。Cookie认证适合传统MVC或Razor Pages应用用户在页面输入账号密码服务端把登录凭证写进带加密签名的Cookie之后每次请求自动带上体验很顺滑。在Program.cs里注册builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie();然后加app.UseAuthentication();和app.UseAuthorization();中间件再配合[Authorize]特性就能控制哪些页面和接口必须登录后才能访问。如果你的架构是前后端分离移动端、小程序都要访问那么JWT更常见。用户登录后服务端签发一个有效期内的令牌前端存在本地请求时放到Authorization: Bearer token头。JWT的好处是无状态、跨域方便缺点是一旦签发在有效期内不好主动作废。所以实际项目里要根据业务选会话型应用用Cookie接口型系统用JWT。不要两个都纠结抓住核心场景选最顺手的。5.2 Web服务器安全HTTPS、请求限流公网上的Web服务器每天都在被各种脚本扫描。首先要做的就是把HTTPS配上让传输内容加密。本地部署可以用自签名证书生产环境用免费证书或云厂商证书证书配置在反向代理层Nginx或IIS都处理得很成熟。其次是限流。你没有限流的时候一个恶意脚本或者失控的前端循环就能把你的进程打到CPU 100%。ASP.NET Core从.NET 7开始内置了RateLimiter中间件可以按固定窗口限制IP每分钟允许的请求数builder.Services.AddRateLimiter(options { options.RejectionStatusCode StatusCodes.Status429TooManyRequests; options.AddFixedWindowLimiter(fixed, opt { opt.PermitLimit 60; opt.Window TimeSpan.FromMinutes(1); }); });这只是示例实际还要按接口、按用户维度设计。为公开API加限流是成本最低、收益最明显的安全手段。另外输入校验也不能省。EF Core的年代SQL注入已经被参数化查询挡住了大半但你还得防范提交超大字段、非法枚举值、恶意脚本。后端永远不信任前端传来的任何数据所有校验都要在后端重新做一遍。5.3 性能优化与缓存企业级接口常见的病就是“越写越慢”。第一件事是定位用日志或者Stopwatch量出接口到底慢在哪。很多时候是数据库查询没有分页一次性查了几万条记录渲染到页面上自然卡死。加.Skip().Take()就能解决问题。然后是N1查询问题EF Core里循环打印每个用户时可能每个用户都触发一次额外查询要用Include或者投影提前把关联数据加载出来return await _db.Orders.Include(o o.User).Where(o o.Status 1).ToListAsync();但Include也不是越多越好这会拼出超宽SQL反而拖慢查询。取舍靠场景验证。缓存是另一个大招。内存缓存IMemoryCache适合缓存字典、基础配置、热门数据。比如一个数据字典接口每分钟被调用上千次但真实数据一小时才变一次完全可以在内存里缓存一小时把数据库压力降下来。具体做法是先查缓存没有再去数据库并设置过期时间。分布式环境记得用Redis这类共享缓存。把“少查数据库”这四个字记在心里很多性能问题都能迎刃而解。5.4 可观测性与监控项目上线不是终点而是真正考验的开始。一个只会在本地跑的程序和你每天要面对几百个用户的线上服务最大的区别就是线上出问题了你能不能在十分钟内定位。企业级Web开发会引入健康检查AddHealthChecks()在Program.cs注册后加一个/health端点负载均衡器每隔几秒探一次知道你的服务还活着。日志也不能随便打。结构化日志指的是把日志输出成JSON带时间戳、级别、请求路径、用户ID。把它接到日志平台之后你可以直接查“从某时间到某时间所有状态码为500的日志”而不是在一堆把字符串拼在句子中间的日志里翻。最少要盯的三件事是错误率上升、慢请求数量、数据库连接数。我们同学里有个说法不会看监控的开发者就像蒙着眼睛开车。这话虽然糙但确实是我踩过多次坑之后的总结。6. 踩坑之后的一点个人心得我自己在刚转C# Web开发那阵子最喜欢干的事就是刷博客、背框架概念迟迟不动手。后来被一个前辈按着用两个晚上把一个小博客从建项目到部署到云服务器完整跑通很多卡住的概念一下子通了。所以我给新人的建议特别朴素别完美主义先把一个最简单的页面从浏览器请求到服务器响应全链路跑通再把数据存进SQLite做一次增删改查最后部署到一台公网服务器。这个闭环走完之后学任何框架都是往这套基本功上加零件。最后再分享一个让我受益很久的习惯每次要做技术选型先写一行注释写下你当前最需要解决的问题是什么再动手。C# Web开发的选择很多Razor Pages、MVC、Minimal API、Blazor但“适合你当前场景”远比“全网最热门”重要。框架只是工具而你能不能把一个请求稳稳接住、把数据安全存下、把错误快速查清才是真正值钱的能力。希望这篇笔记能帮你少撞一些我当年撞过的墙。
返回列表