尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ASP.NET Core零基础教程:路由与依赖注入详解,从零搭建企业级后台

ASP.NET Core零基础教程:路由与依赖注入详解,从零搭建企业级后台 在我们开始之前先看一个很常见的现象。很多同学学 ASP.NET Core第一周就放弃了。不是因为 C# 难也不是因为框架复杂而是因为市面上大多数教程默认你“已经会了”。它们一上来就给你看一个几十行的 Controller里面同时混着数据库查询、业务逻辑、JWT 认证、Redis 缓存然后告诉你“照着写就行了”。新手看完反应通常是一致的路由是什么依赖注入又是什么为什么一个接口要拆成三个类这些概念不搞懂代码根本没法独立写。这篇教程的目标非常明确零基础、纯小白、保姆级。我会把 ASP.NET Core 里最核心的两个概念——路由Routing和依赖注入Dependency Injection掰开揉碎讲清楚然后用它们一步步搭出一个适合企业级后台项目的骨架。你不需要有任何 C# 基础也不需要提前理解什么“容器”“中间件”“生命周期”跟着文章走完你会发现自己不仅能看懂代码还能动手修改和扩展。文章会按这个顺序展开先讲清楚路由和依赖注入到底解决了什么问题再带着你从安装环境开始创建项目、写代码、跑起来、验证结果最后给出后台开发中最常见的坑和最佳实践。建议把文章收藏起来边看边敲效果最好。1. 这篇文章真正要解决的问题先回答一个最直接的问题为什么要学 ASP.NET Core它是微软推出的跨平台 Web 开发框架可以运行在 Windows、Linux、macOS 上。相比老牌的 ASP.NET Framework它的性能更强、模块化程度更高而且原生内置了依赖注入容器、中间件管道、配置系统等企业级功能。换句话说你用 ASP.NET Core 写的后台服务既能做 API也能做 MVC 网站还能做微服务中的某个独立模块适用范围非常广。对于零基础的同学来说它最大的优势其实是“有完整的主线”。后端开发里很多概念在 ASP.NET Core 中都有官方默认实现比如依赖注入是内置的日志是内置的配置读取是内置的。你用 Java 需要自己组合各种框架在 ASP.NET Core 里开箱即用的是同一个技术栈减少了很多“配置框架”的负担。这篇文章会聚焦三个关键点路由用户请求 URL 后框架怎么知道把请求交给哪个方法处理。依赖注入多个类之间的依赖关系怎么管理为什么不让类自己 new 对象。企业级后台骨架如何用 Controller Service Repository 的分层方式组织代码让项目在后期的扩展和维护中不失控。你读完这篇文章后能自己完成一件小事创建项目、定义路由、通过依赖注入调用业务服务、返回 JSON 数据。这件事虽然不大却是所有企业级后台的起点。2. 基础概念路由与依赖注入为什么它们是后台开发的核心2.1 路由是什么通俗地说路由就是“URL 到代码的映射”。假设你在浏览器里输入https://localhost:5001/api/user/1这个请求发到服务器后ASP.NET Core 会解析这个 URL判断出控制器Controller大概率是UserController动作方法Action大概率是某个处理 GET 请求的方法1是传递给方法的参数如果你没学过路由第一次看到这种机制会觉得神奇为什么 URL 是这样代码就能自动执行其实背后的规则就是路由系统根据你注册的路由模板去匹配。路由在 ASP.NET Core 里有两种主要形式约定式路由在Program.cs或Startup.cs中定义一个统一的 URL 模板例如/api/{controller}/{action}/{id?}所有请求都按这个模板匹配。属性路由直接在 Controller 或 Action 上用[Route]特性标注 URL控制更精细API 项目几乎都用这种方式。对于新手我建议直接学属性路由。原因是现代 ASP.NET Core API 开发中约定式路由更多用于 MVC 页面应用而属性路由在做前后端分离项目时更直观、更容易调试。2.2 依赖注入是什么依赖注入是一个听起来很高级、实际上很朴素的编程思想。先看一段很常见的“反面教材”public class OrderService { private readonly UserRepository _userRepo; public OrderService() { _userRepo new UserRepository(); } }看起来没什么问题但想象一下如果UserRepository的构造函数里需要传入数据库连接字符串而这个连接字符串又来自配置文件你怎么在这里new如果UserRepository里面还需要调用日志类、缓存类每个类都自己new整个项目的对象创建关系就会变成一张混乱的蜘蛛网。依赖注入做的事情是你不再自己new对象而是告诉框架“我需要一个UserRepository”框架在合适的时候把实例给你。这个“合适的实例”怎么来答案是服务容器。你在项目启动时注册各种类型比如“当有人需要IUserRepository就给他返回UserRepository的一个实例”。到了运行时ASP.NET Core 会自动帮你把依赖链条串起来。依赖注入解决了三个问题解耦类与类之间不再直接 new而是依赖抽象接口。可测试单元测试时你可以注入一个假的、内存版的仓储不必真的连数据库。生命周期管理框架帮你管理对象是每次请求都新建还是整个应用共享一个实例。在企业级后台项目中依赖注入不是“可选项”而是默认的开发方式。你甚至不需要额外安装任何 NuGet 包——ASP.NET Core 内置了依赖注入容器。3. 环境准备从零搭建开发环境这一部分带你把环境装好。版本以你安装的 SDK 实际版本为准本文演示的命令在 .NET 6、.NET 7、.NET 8、.NET 9 上都通用新版 .NET 的命令没有明显变化。你需要准备.NET SDK到微软官网下载选择你操作系统对应的版本。Windows 就选 Windows 版macOS 就选 macOS 版。安装时一路 Next 即可。IDE 工具Windows 用户推荐 Visual Studio 2022 社区版免费功能完整。跨平台用户推荐 Visual Studio Code C# 扩展轻量也够用。终端Windows 下使用 PowerShell 或 CMDmacOS/Linux 下使用 Terminal。安装完成后打开终端验证一下是否成功dotnet --version如果终端能输出版本号比如9.0.100说明 SDK 安装成功。再查看一下你机器上有哪些 SDKdotnet --list-sdks输出示例9.0.100 [/usr/local/share/dotnet/sdk] 8.0.100 [/usr/local/share/dotnet/sdk]这个命令在以后排查版本问题时很有用。4. 创建你的第一个 ASP.NET Core 项目4.1 使用命令行创建项目打开终端进入你准备存放代码的目录执行dotnet new webapi -n MyFirstApi这里的webapi是项目模板-n MyFirstApi指定项目名称。创建完成后进入项目目录cd MyFirstApi你可以用 IDE 打开这个项目也可以在终端里直接使用dotnet run启动。4.2 项目结构说明先看一下项目里的关键文件不要被一堆文件吓到真正核心的就这几个文件/目录作用Program.cs应用的入口负责配置服务、配置中间件管道Controllers/存放控制器控制器接收请求、调用服务、返回结果appsettings.json配置文件存放连接字符串、日志级别、自定义配置MyFirstApi.csproj项目文件声明依赖的 NuGet 包和编译配置在 .NET 6 之后Program.cs采用极简风格不再有Startup.cs所有配置都在一个文件里完成。第一次看可能不习惯但看多了你会觉得非常清爽。5. 路由精讲两种路由方式与实用配置这一节是重点。我们直接看代码。5.1 模板自带的属性路由新建的 WebAPI 项目中Program.cs会有这样一行app.MapControllers();这一行的作用是把所有控制器里通过[Route]定义的属性路由注册到路由表中。默认控制器文件是Controllers/WeatherForecastController.cs它长这样using Microsoft.AspNetCore.Mvc; namespace MyFirstApi.Controllers; [ApiController] [Route(api/[controller])] public class WeatherForecastController : ControllerBase { [HttpGet] public IEnumerableWeatherForecast Get() { // 返回数据... } }看这一段代码你只需要理解一个核心规则[Route(api/[controller])]表示这个控制器的基础地址是api/weatherforecast[controller]是占位符运行时会被替换成控制器名去掉Controller后缀的小写形式。[HttpGet]表示监听 HTTP GET 请求。所以访问GET /api/weatherforecast框架就会执行Get()方法。5.2 自己写一个带路由参数的控制器为了真正理解路由我们动手写一个用户控制器。在Controllers目录下新建文件UserController.csusing Microsoft.AspNetCore.Mvc; namespace MyFirstApi.Controllers; [ApiController] [Route(api/users)] public class UserController : ControllerBase { [HttpGet] public IActionResult GetList() { return Ok(new[] { 张三, 李四, 王五 }); } [HttpGet({id})] public IActionResult GetById(int id) { if (id 0) { return BadRequest(id 必须大于 0); } return Ok($用户 ID 为 {id}); } [HttpPost] public IActionResult Create([FromBody] string name) { if (string.IsNullOrWhiteSpace(name)) { return BadRequest(name 不能为空); } return Ok($创建用户成功{name}); } }这段代码里蕴含的路由知识[HttpGet]不带参数匹配GET /api/users[HttpGet({id})]匹配GET /api/users/1其中1会被绑定到id参数[HttpPost]匹配POST /api/users请求体里的 JSON 字符串会绑定到name这就是属性路由的核心用法。你不需要写复杂的路由表只需在每个 Action 上说明“我监听什么方法、URL 长什么样”。5.3 路由约束与默认值后台开发中经常需要验证 URL 里的参数格式。ASP.NET Core 支持在路由模板里直接加约束[HttpGet({id:int})] public IActionResult GetById(int id) { return Ok($用户 ID 为 {id}); }{id:int}表示id必须是整数。如果客户端访问/api/users/abc框架不会进入这个方法而会直接返回 404。类似约束还有约束含义示例{id:int}必须是整数10{name:length(1,20)}长度在 1 到 20 之间zhangsan{email:email}必须是合法邮箱格式ab.com{date:datetime}必须是合法日期2025-01-01新手阶段掌握int约束就够了其他约束在真正需要时再查文档即可。6. 依赖注入精讲从接口到容器6.1 为什么先写接口企业级后台项目中几乎每个 Service 类都会对应一个接口。这不是多余的抽象而是为了控制依赖方向。比如你写了一个UserService它需要访问数据库。如果控制器直接依赖UserService这个具体类那么以后你想给UserService增加缓存逻辑、日志逻辑就要修改控制器的代码如果依赖IUserService接口控制器的代码完全不用变只需要在注册服务时换成新的实现类。这个过程用一句话总结面向接口编程而不是面向实现编程。6.2 三种生命周期ASP.NET Core 依赖注入有三种生命周期初学者必须分清楚生命周期方法什么时候创建实例典型用途瞬态AddTransient每次请求都创建新实例无状态服务作用域AddScoped每个 HTTP 请求内同一个实例数据库上下文DbContext单例AddSingleton整个应用生命周期同一个实例配置对象、缓存服务新手最容易踩的坑是把DbContext注册成Singleton结果出现并发问题或者把有状态的服务注册成Transient导致每次拿到的都是新对象之前保存的数据丢失。6.3 后台项目中的依赖注入实操先在项目里创建一个文件夹Services然后新增接口和实现类。文件路径Services/IUserService.csnamespace MyFirstApi.Services; public interface IUserService { string GetUserInfo(int id); }文件路径Services/UserService.csnamespace MyFirstApi.Services; public class UserService : IUserService { public string GetUserInfo(int id) { return $用户 {id} 的信息来自业务层; } }然后在Program.cs中注册using MyFirstApi.Services; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 注册业务服务推荐 Scoped builder.Services.AddScopedIUserService, UserService(); var app builder.Build(); app.MapControllers(); app.Run();注册完成之后控制器里通过构造函数注入拿到服务using Microsoft.AspNetCore.Mvc; using MyFirstApi.Services; namespace MyFirstApi.Controllers; [ApiController] [Route(api/users)] public class UserController : ControllerBase { private readonly IUserService _userService; public UserController(IUserService userService) { _userService userService; } [HttpGet({id:int})] public IActionResult GetById(int id) { var data _userService.GetUserInfo(id); return Ok(data); } }注意_userService这个字段用readonly修饰。这是 C# 里非常标准的依赖注入写法构造函数把依赖接口的实例传进来赋值给字段。控制器里不需要new UserService()容器会在创建UserController时自动把UserService实例传进来。7. 综合实战搭建企业级后台项目骨架现在把前面两部分串起来做一个更接近真实后台的小项目商品后台。这个项目会包含Models实体模型Repositories数据访问层暂时用内存模拟Services业务逻辑层Controllers接口层7.1 定义实体模型文件路径Models/Product.csnamespace MyFirstApi.Models; public class Product { public int Id { get; set; } public string Name { get; set; } string.Empty; public decimal Price { get; set; } }7.2 编写仓储接口与实现文件路径Repositories/IProductRepository.csusing MyFirstApi.Models; namespace MyFirstApi.Repositories; public interface IProductRepository { ListProduct GetAll(); Product? GetById(int id); }文件路径Repositories/ProductRepository.csusing MyFirstApi.Models; namespace MyFirstApi.Repositories; public class ProductRepository : IProductRepository { private readonly ListProduct _products new() { new Product { Id 1, Name 笔记本电脑, Price 5999m }, new Product { Id 2, Name 机械键盘, Price 399m }, new Product { Id 3, Name 显示器, Price 1999m } }; public ListProduct GetAll() { return _products; } public Product? GetById(int id) { return _products.FirstOrDefault(p p.Id id); } }这里用内存列表模拟数据库目的是让你先看明白依赖注入和分层是怎么协作的。以后接真实数据库时只需要改ProductRepository内部实现控制器和服务的代码完全不用动。7.3 编写业务服务接口与实现文件路径Services/IProductService.csusing MyFirstApi.Models; namespace MyFirstApi.Services; public interface IProductService { ListProduct GetProducts(); Product? GetProductById(int id); }文件路径Services/ProductService.csusing MyFirstApi.Models; using MyFirstApi.Repositories; namespace MyFirstApi.Services; public class ProductService : IProductService { private readonly IProductRepository _productRepository; public ProductService(IProductRepository productRepository) { _productRepository productRepository; } public ListProduct GetProducts() { return _productRepository.GetAll(); } public Product? GetProductById(int id) { return _productRepository.GetById(id); } }注意ProductService通过构造函数依赖IProductRepository它同样不关心ProductRepository内部怎么实现的。7.4 编写控制器文件路径Controllers/ProductController.csusing Microsoft.AspNetCore.Mvc; using MyFirstApi.Models; using MyFirstApi.Services; namespace MyFirstApi.Controllers; [ApiController] [Route(api/products)] public class ProductController : ControllerBase { private readonly IProductService _productService; public ProductController(IProductService productService) { _productService productService; } [HttpGet] public IActionResult GetAll() { return Ok(_productService.GetProducts()); } [HttpGet({id:int})] public IActionResult GetById(int id) { var product _productService.GetProductById(id); if (product null) { return NotFound($找不到 ID 为 {id} 的商品); } return Ok(product); } }7.5 在 Program.cs 中注册服务文件路径Program.csusing MyFirstApi.Repositories; using MyFirstApi.Services; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 注册仓储层每个请求一个实例 builder.Services.AddScopedIProductRepository, ProductRepository(); // 注册业务层每个请求一个实例 builder.Services.AddScopedIProductService, ProductService(); var app builder.Build(); app.MapControllers(); app.Run();到这里一个最小的企业级后台骨架就完成了。它有三层结构依赖方向是Controller → Service → Repository每一层只依赖下一层的接口而不依赖具体实现。这就是依赖注入带来的最大好处。8. 运行结果与验证8.1 启动项目在终端执行dotnet run看到类似输出说明启动成功Now listening on: http://localhost:5xxx具体端口以控制台输出为准ASP.NET Core 默认会分配一个随机端口。8.2 测试接口打开浏览器访问http://localhost:5xxx/api/products页面会返回 JSON 数据[ { id: 1, name: 笔记本电脑, price: 5999.0 }, { id: 2, name: 机械键盘, price: 399.0 }, { id: 3, name: 显示器, price: 1999.0 } ]再访问单个商品http://localhost:5xxx/api/products/2返回{ id: 2, name: 机械键盘, price: 399.0 }访问一个不存在的 IDhttp://localhost:5xxx/api/products/999会返回 404 状态码和提示信息。如果在测试时遇到问题第一步先看终端里的日志。ASP.NET Core 的日志已经把请求和异常输出得比较明确通常能直接定位到错误原因。9. 常见问题与排查方法问题现象可能原因排查方式解决方案启动时报“端口被占用”上次运行的项目没有完全退出查看终端日志找到具体端口停止占用端口的进程或修改launchSettings.json中的端口访问接口返回 404控制器的路由模板和请求 URL 不匹配检查[Route]、[HttpGet]的路径在浏览器中访问准确的 URL注意大小写和路径报错“InvalidOperationException: Unable to resolve service for type”服务没有在Program.cs中注册或者注册类型和构造函数参数类型不一致看异常信息它明确告诉你无法解析的类型在Program.cs中添加对应注册例如AddScopedIUserService, UserService()报错“Cannot resolve scoped service from root provider”在Singleton服务中注入了Scoped服务查看服务生命周期的注册代码调整生命周期确保外层服务生命周期不低于内层服务请求返回 500但浏览器看不到详情开发环境默认启用开发异常页面发布环境需要自己配置日志查看控制台日志或系统日志开发阶段确认使用app.UseDeveloperExceptionPage()生产环境记录完整异常日志JSON 返回字段顺序不对ASP.NET Core 默认按属性定义顺序序列化检查模型定义使用[JsonPropertyName]或 DTO 指定输出字段新手最常遇到的就是第三个问题。只要你新增了一个服务接口却忘了在Program.cs里注册运行时一定会看到Unable to resolve service。解决方法很简单永远记得“先注册再注入”。10. 最佳实践与工程建议10.1 依赖注入的生命周期选择后台项目里大部分 Service 和 Repository 类使用AddScoped就够了。它保证同一个 HTTP 请求内使用的是同一个实例既不会像Transient那样频繁创建对象也不会像Singleton那样在多线程并发下共享状态造成隐患。10.2 构造函数不要有太多参数如果你的控制器构造函数里面有 5 个以上的参数说明这个类可能承担了太多职责。建议把相关的依赖封装成服务或者再做一层聚合服务。依赖注入虽然方便但不是让你把所有东西都塞进一个类。10.3 控制器只做参数校验和结果返回很多初学者容易把数据库查询直接写在 Controller 里。短期看起来没问题但项目一复杂你会发现同一个查询可能被多个接口用到重复代码越来越多。正确的做法是Controller 只处理 HTTP 请求语义业务逻辑放到 Service 层数据访问放到 Repository 层。10.4 使用 DTO 而不是直接把实体返回给前端企业级后台项目中实体模型字段往往和接口返回值不完全一致。比如Product里有内部备注字段不应该暴露给前端。更优雅的做法是定义 DTOData Transfer Objectnamespace MyFirstApi.Models.Dtos; public class ProductDto { public int Id { get; set; } public string Name { get; set; } string.Empty; public decimal Price { get; set; } }然后在 Service 层把实体映射成 DTO 再返回。这样前端看到的永远是接口层约定好的数据不会受实体变动影响。10.5 配置文件不要写死不要把数据库连接字符串、第三方服务的 Key 写死在代码里。放到appsettings.json并通过IConfiguration读取var connectionString builder.Configuration.GetConnectionString(Default);企业环境下连接字符串通常会放到环境变量或配置中心但读取方式始终是同一个模式。10.6 日志一定要打但不是每个方法都打至少要在 Service 层记录关键操作比如创建、删除、异常。日志级别上调试信息用LogDebug一般状态用LogInformation异常用LogError。依赖注入已经内置了ILoggerT直接用即可public class ProductService : IProductService { private readonly ILoggerProductService _logger; public ProductService(ILoggerProductService logger) { _logger logger; } }11. 总结与下一步学习方向这篇文章从零开始讲透了 ASP.NET Core 中两个核心概念路由解决的是“URL 来了该交给谁处理”。属性路由用[Route]、[HttpGet]等特性标注 URL直观、灵活适合 API 后台开发。依赖注入解决的是“对象依赖关系怎么管理”。你不需要手动new服务类只需要在Program.cs中注册接口与实现的关系框架就会在运行时自动注入。再加上AddScoped、AddTransient、AddSingleton三种生命周期你可以根据自己的业务需要精确控制对象的创建时机。最后我们通过一个商品后台的示例把 Controller、Service、Repository 三层结构串了起来。这个骨架虽然简单但它可以继续扩展成更真实的项目把内存数据替换成 Entity Framework Core SQL Server增加异常处理中间件增加 JWT 认证加入自动映射工具编写单元测试。建议你现在打开终端亲手创建项目把文章里的代码敲一遍。先跑通再扩展。如果运行中遇到任何报错不要慌先看终端日志再回到文章第 9 节对照排查。下一步可以优先学习这几个方向EF Core 与数据库迁移中间件管道与异常处理JWT 身份认证与授权使用 DTO 和 AutoMapper 进行模型映射部署到 Linux 服务器或 Docker 容器把这篇文章吃透之后你已经具备了独立阅读企业级 ASP.NET Core 项目的能力。再往下接触任何新概念都不会像一开始那么吃力了。
返回列表