- 示例工程
- 后端
- 前端
【免费下载链接】NorthwindTraders
Northwind Traders is a sample application built using ASP.NET Core and Entity Framework Core.
本文以 NorthwindTraders 示例项目(ASP.NET Core + Entity Framework Core 的 Clean Architecture 演示仓库)中的基础设施层为切入点,系统讲解该层"访问文件系统、Web 服务、SMTP 等外部资源,且实现必须基于应用层定义的接口"这一核心设计原则。读完本文,你将掌握 Clean Architecture 中基础设施层的职责边界、依赖倒置的具体落地方式,以及 CSV 导出、通知服务、时间服务、身份认证等四类基础设施服务的真实实现与注册方法。
一、原文档核心:基础设施层的职责定位
仓库中 Src/Infrastructure/README.md 用两句话定义了这一层的全部使命:
This layer contains classes for accessing external resources such as file systems, web services, smtp, and so on. These classes should be based on interfaces defined within the application layer.
翻译过来即两条铁律:
- 职责边界:基础设施层只负责与"外部世界"打交道——文件系统读写、Web 服务调用、SMTP 邮件发送等一切外部资源访问,全部收敛于此;
- 依赖方向:这些实现类必须基于应用层(Application)定义的接口来编写,而不是反过来由应用层依赖基础设施层的具体类型。
这与 Src/Application/README.md 的表述互为印证:
This layer defines interfaces that are implemented by outside layers. For example, if the application need to access a notification service, a new interface would be added to application and an implementation would be created within infrastructure.
也就是说:应用层"定义需求",基础设施层"负责兑现"。接口与实现的分离,让业务逻辑可以完全脱离对具体 IO、邮件、认证实现的编译期依赖,这正是 Clean Architecture 中依赖倒置原则(Dependency Inversion)的直观体现。
二、四组接口—实现映射:依赖倒置的落地证据
NorthwindTraders 的基础设施层通过四组"应用层接口 → 基础设施实现"的组合,完整演示了上述原则。全部注册集中在 Src/Infrastructure/DependencyInjection.cs 的AddInfrastructure扩展方法中:
| 应用层接口 | 基础设施层实现 | 注册生命周期 | 职责 |
|---|---|---|---|
ICsvFileBuilder | CsvFileBuilder | Transient | 文件系统(生成 CSV 文件字节流) |
INotificationService | NotificationService | Transient | 通知/邮件(SMTP 类外部资源) |
IDateTime | MachineDateTime | Transient | 系统时钟(可测试性抽象) |
IUserManager | UserManagerService | Scoped | 用户管理(ASP.NET Core Identity) |
注册代码如下:
services.AddScoped<IUserManager, UserManagerService>(); services.AddTransient<INotificationService, NotificationService>(); services.AddTransient<IDateTime, MachineDateTime>(); services.AddTransient<ICsvFileBuilder, CsvFileBuilder>();接口本身定义在应用层与公共层中,例如 Src/Application/Common/Interfaces/ICsvFileBuilder.cs、Src/Application/Common/Interfaces/INotificationService.cs、Src/Application/Common/Interfaces/IUserManager.cs,以及公共层 Src/Common/IDateTime.cs。基础设施层通过项目引用(ProjectReference)依赖 Application 与 Common,从而拿到这些接口;而应用层对基础设施层则保持零引用,依赖关系完全靠运行时依赖注入容器组装。这一点在 Src/Infrastructure/Infrastructure.csproj 中清晰可见:
<ItemGroup> <ProjectReference Include="..\Application\Application.csproj" /> <ProjectReference Include="..\Common\Common.csproj" /> </ItemGroup>同时,该 csproj 还声明了四个 NuGet 包依赖:CsvHelper 12.1.2(CSV 生成)、Microsoft.AspNetCore.ApiAuthorization.IdentityServer 3.0.0(IdentityServer4 的 API 授权)、Microsoft.AspNetCore.Identity.EntityFrameworkCore 3.0.0(Identity 存储)、Microsoft.EntityFrameworkCore.SqlServer 3.0.0(SQL Server 访问),目标框架为netcoreapp3,程序集名为Northwind.Infrastructure。
三、案例一:文件系统访问——CSV 产品文件导出
这是基础设施层"文件系统访问"职责最完整的一条调用链示例。
3.1 应用层定义接口与查询
应用层在 ICsvFileBuilder.cs 中声明:
public interface ICsvFileBuilder { byte[] BuildProductsFile(IEnumerable<ProductRecordDto> records); }对应的查询处理器 GetProductsFileQueryHandler.cs 通过构造函数注入ICsvFileBuilder,将"查数据"与"生成文件"解耦:
var records = await _context.Products .ProjectTo<ProductRecordDto>(_mapper.ConfigurationProvider) .ToListAsync(cancellationToken); var fileContent = _fileBuilder.BuildProductsFile(records); var vm = new ProductsFileVm { Content = fileContent, ContentType = "text/csv", FileName = $"{_dateTime.Now:yyyy-MM-dd}-Products.csv" };注意这里同时注入了IDateTime,文件名采用yyyy-MM-dd-Products.csv的日期前缀格式,用于区分不同日期的导出文件。
3.2 基础设施层实现 CSV 生成
实现类 CsvFileBuilder.cs 使用 CsvHelper 将ProductRecordDto记录写入内存流并返回字节数组:
public class CsvFileBuilder : ICsvFileBuilder { public byte[] BuildProductsFile(IEnumerable<ProductRecordDto> records) { using var memoryStream = new MemoryStream(); using (var streamWriter = new StreamWriter(memoryStream)) { using var csvWriter = new CsvWriter(streamWriter); csvWriter.Configuration.RegisterClassMap<ProductFileRecordMap>(); csvWriter.WriteRecords(records); } return memoryStream.ToArray(); } }3.3 字段映射与本地化格式
CsvHelper 的字段映射由 ProductFileRecordMap.cs 负责。它在AutoMap()自动映射全部属性的基础上,对UnitPrice做了列名与格式化定制:
AutoMap(); Map(m => m.UnitPrice).Name("Unit Price").ConvertUsing(c => (c.UnitPrice ?? 0).ToString("C"));即UnitPrice输出列名为Unit Price,格式化为货币字符串(ToString("C"));UnitPrice为decimal?可空类型,为 null 时按 0 处理。其余列(Category、Name、Discontinued)按ProductRecordDto属性自动映射。该 DTO 定义于 ProductRecordDto.cs,其 AutoMapper 映射规则把领域实体Product的ProductName映射为Name、把Category.CategoryName映射为Category(分类为空时输出空字符串)。
3.4 对外暴露:控制器下载接口
ProductsController.cs 中的Download端点把整个链路串起来,以文件下载形式返回 CSV:
[HttpGet] [AllowAnonymous] public async Task<FileResult> Download() { var vm = await Mediator.Send(new GetProductsFileQuery()); return File(vm.Content, vm.ContentType, vm.FileName); }整条链路为:HTTP 请求 → MediatR 查询 → EF Core 投影查询 →ICsvFileBuilder(基础设施实现)生成字节流 →FileResult下载。业务层完全不感知 CsvHelper 的存在,这就是"接口在应用层、实现在基础设施层"的完整实战闭环。
四、案例二:通知服务——SMTP 类外部资源的接入点
通知服务对应原文档提到的 "smtp" 类外部资源。应用层定义 INotificationService.cs:
public interface INotificationService { Task SendAsync(MessageDto message); }消息模型MessageDto定义于 MessageDto.cs,包含From、To、Subject、Body四个字段——这正是邮件通知的最小信息结构。
基础设施层的 NotificationService.cs 当前提供的是桩(stub)实现:
public class NotificationService : INotificationService { public Task SendAsync(MessageDto message) { return Task.CompletedTask; } }从源码结构看,这是有意保留的占位实现:接口契约已就位,具体邮件发送(SMTP 客户端、模板渲染、重试策略等)留待接入真实邮件服务时替换。这正是依赖倒置的典型收益——应用层代码早已依赖抽象编写完毕,后续只需在基础设施层替换具体实现,业务代码无需任何改动。
五、案例三:时间服务——可测试性设计
应用层经常需要获取当前时间(如上述导出文件的日期前缀),但如果直接调用DateTime.Now,单元测试将难以控制时间。为此,公共层定义 IDateTime.cs:
public interface IDateTime { DateTime Now { get; } }基础设施层的 MachineDateTime.cs 提供默认的"机器时间"实现:
public class MachineDateTime : IDateTime { public DateTime Now => DateTime.Now; public int CurrentYear => DateTime.Now.Year; }从源码结构看,MachineDateTime在实现接口要求的Now之外还额外暴露了CurrentYear属性。测试环境下可以替换为可控的假时间实现,从而对依赖时间的业务逻辑(如文件名、审计字段)进行确定性断言。
六、案例四:身份认证与用户管理——Identity + IdentityServer
基础设施层的Identity子目录承载了完整的认证授权基础设施,涵盖四类文件。
6.1 用户实体与身份数据库上下文
ApplicationUser.cs 直接继承 ASP.NET Core Identity 的IdentityUser,当前未扩展任何额外属性:
public class ApplicationUser : IdentityUser { }ApplicationDbContext.cs 继承ApiAuthorizationDbContext<ApplicationUser>(IdentityServer4 的 API 授权 + Identity 的组合上下文),其迁移位于 Src/Infrastructure/Identity/Migrations,20190905003230_CreateIdentitySchema迁移创建了AspNetRoles、AspNetUsers(含PasswordHash、SecurityStamp、TwoFactorEnabled、LockoutEnd等标准字段)等 Identity 表结构。
需要说明的是:该ApplicationDbContext服务于身份认证数据;而业务领域数据(客户、订单、产品等)由 Persistence 层的NorthwindDbContext管理,实现 INorthwindDbContext.cs,两者职责不同,不可混淆。
6.2 用户管理服务:接口与实现的完整对应
应用层 IUserManager.cs 声明了创建与删除用户的契约:
Task<(Result Result, string UserId)> CreateUserAsync(string userName, string password); Task<Result> DeleteUserAsync(string userId);基础设施层 UserManagerService.cs 注入 ASP.NET Core 的UserManager<ApplicationUser>实现契约:创建用户时以userName同时作为UserName与Email(示例项目按此约定存储);删除用户时先按Id查询,存在才执行删除,不存在则直接返回成功(幂等语义)。
值得关注的是错误处理桥接:IdentityResult(Identity 的错误格式)通过 IdentityResultExtensions.cs 转换为应用层的 Result.cs 统一结果模型:
return result.Succeeded ? Result.Success() : Result.Failure(result.Errors.Select(e => e.Description));这样 Identity 的错误描述被统一收口为Result.Errors字符串数组,应用层无需感知 Identity 的细节类型。
6.3 注册与测试环境专用客户端
AddInfrastructure中完整注册了身份相关服务:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("NorthwindDatabase"))); services.AddDefaultIdentity<ApplicationUser>() .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddIdentityServer() .AddApiAuthorization<ApplicationUser, ApplicationDbContext>(); services.AddAuthentication() .AddIdentityServerJwt();其中连接字符串NorthwindDatabase来自配置文件,默认配置见 Src/WebUI/appsettings.json:
"ConnectionStrings": { "NorthwindDatabase": "Server=(localdb)\\mssqllocaldb;Database=NorthwindTraders;Trusted_Connection=True;MultipleActiveResultSets=true;Application Name=NorthwindTraders;" }AddInfrastructure还有一个值得注意的环境分支:当宿主环境为Test时,额外注册一个用于集成测试的 IdentityServer 客户端(ClientId = "Northwind.IntegrationTests",授权方式ResourceOwnerPassword,密钥为secret的 SHA-256 哈希),并注入测试用户jason@northwind/Northwind1!(含 Email Claim)。这套机制正是集成测试能够拿到真实 JWT 令牌的基础——Tests/WebUI.IntegrationTests/Controllers/Customers/Delete.cs 中通过_factory.GetAuthenticatedClientAsync()获取已认证的 HttpClient,进而测试DELETE /api/customers/delete/{id}的成功(有效 IDALFKI)与 404(无效 IDAAAAA)两条路径。
前端登录页面 Src/WebUI/Areas/Identity/Pages/Account/Login.cshtml.cs 则通过SignInManager<ApplicationUser>.PasswordSignInAsync完成实际登录,并在配置中启用了账户锁定(lockoutOnFailure: true)。
七、运行前提与注意事项
基于仓库现状,使用该层时有几点需要留意:
- 框架版本:项目 csproj 声明目标框架为
netcoreapp3,依赖包版本均为 3.0.0 系列(CsvHelper 12.1.2 除外)。仓库根 README.md 已注明该项目已归档、不再维护,若 fork 使用需自行升级依赖; - 数据库:身份数据需要 SQL Server(默认连接 LocalDB 的
NorthwindTraders库),首次运行前需应用 Src/Infrastructure/Identity/Migrations 中的迁移;业务数据迁移位于 Src/Persistence/Migrations; - 测试环境变量:集成测试依赖
Test环境分支注册的测试客户端与测试用户,运行测试前需确保以Test环境启动 Web 宿主,相关配置见 Src/WebUI/appsettings.Test.json(含 IdentityServer 开发密钥Key.Type = Development)。
八、小结
NorthwindTraders 的基础设施层虽然只有四行 README 描述,却浓缩了 Clean Architecture 的两个关键实践:外部资源访问统一收口到基础设施层(文件系统、通知、时间、认证),以及实现必须基于应用层接口(ICsvFileBuilder、INotificationService、IDateTime、IUserManager四组映射)。配合AddInfrastructure的依赖注入注册、CsvHelper 的文件生成管线、Identity + IdentityServer 的认证体系,这套结构为读者提供了一个可直接对照的"接口在应用层、实现在基础设施层"的最小完整范例。
- 示例工程
- 后端
- 前端
【免费下载链接】NorthwindTraders
Northwind Traders is a sample application built using ASP.NET Core and Entity Framework Core.
相关推荐
如何在5分钟内搭建企业级文档智能解析系统:MinerU实战深度指南
如何在5分钟内搭建企业级文档智能解析系统:MinerU实战深度指南 面对海量PDF、Office文档的结构化解析需求,传统OCR工具往往难以处理复杂的版面布局、
人工智能大模型OCR计算机视觉Dante Cloud设施Starter:基础设施切换依赖的最佳实践
Dante Cloud设施Starter:基础设施切换依赖的最佳实践 引言:微服务架构中的基础设施挑战 在微服务架构演进过程中,基础设施组件的选择和切换往往成为
Nexent Kubernetes 基础设施 Helm Chart 实战:nexent-infrastructure 部署、配置与源码级原理
Nexent Kubernetes 基础设施 Helm Chart 实战:nexent infrastructure 部署、配置与源码级原理 本文围绕 Nexe
人工智能AI AgentAgent 编排低代码AI 技能RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考