ASP.NET Core Clean Architecture
文章目录
- 项目地址
- 一、项目主体
- 1. CQRS
- 1.1 Repository数据库接口
- 1.2 GetEventDetail 完整的Query流程
- 1.3 创建CreateEventCommand并使用validation
- 2. EFcore层
- 2.1 BaseRepository
- 2.2 CategoryRepository
- 2.3 OrderRepository
- 3. Email/Excel导出
- 3.1 Email
- 1. IEmail接口层
- 2. Email的Model层
- 3. 具体Email的实现层
- 4. 配置settings
- 3.2 Excel导出
- 1. 导出excel接口层
- 2. Controller层
- 3. Query层
- 4. 实现IExcelService
- 4. 定义response/全局错误处理中间件
- 4.1 统一response
- 1. 定义统一的返回类
- 2. 使用
- 4.2 全局错误处理中间件
- 5. 用户权限相关
- 5.1 用户权限相关的接口层
- 5.2 登录/注册/jwt 实体类定义
- 5.2 用户实体
- 5.3 用户认证所有接口实现的地方
- 5.4 用户服务注册
- 6. 添加日志
- 7. 版本控制
- 8. 分页
- 9. 配置中间件和服务注册
- 二、测试
- 1. Unitest
- 2. Integration Tests
项目地址
-
教程作者:ASP.NET Core Clean Architecture 2022-12
-
教程地址:
https://www.bilibili.com/video/BV1YZ421M7UA?spm_id_from=333.788.player.switch&vd_source=d14620e2c9f01dee5d2a104075027ad1&p=16
- 代码仓库地址:
- 所用到的框架和插件:
一、项目主体
- 整个项目4层结构

- Application层

1. CQRS
1.1 Repository数据库接口
- Application层的Contracts里的Persistence,存放数据库的接口

IAsyncRepository:基类主要功能,规定 增删改查/单一查询/分页
namespace GloboTicket.TicketManagement.Application.Contracts.Persistence
{public interface IAsyncRepository<T> where T : class{Task<T?> GetByIdAsync(Guid id);Task<IReadOnlyList<T>> ListAllAsync();Task<T> AddAsync(T entity);Task UpdateAsync(T entity);Task DeleteAsync(T entity);Task<IReadOnlyList<T>> GetPagedReponseAsync(int page, int size);}
}
ICategoryRepository.cs:添加自己独特的GetCategoriesWithEvents 方法
namespace GloboTicket.TicketManagement.Application.Contracts.Persistence
{public interface ICategoryRepository : IAsyncRepository<Category>{Task<List<Category>> GetCategoriesWithEvents(bool includePassedEvents);}
}
IEventRepository.cs:添加Event自己的方法
namespace GloboTicket.TicketManagement.Application.Contracts.Persistence
{public interface IEventRepository : IAsyncRepository<Event>{Task<bool> IsEventNameAndDateUnique(string name, DateTime eventDate);}
}
IOrderRepository.cs: 没有自己的方法,直接继承使用
namespace GloboTicket.TicketManagement.Application.Contracts.Persistence
{public interface IOrderRepository: IAsyncRepository<Order>{}
}
1.2 GetEventDetail 完整的Query流程
-
项目层级

-
EventDetailVm.cs:用于返回给接口的数据

CategoryDto.cs:表示在GetEventDetail里需要用到的Dto

GetEventDetailQuery.cs:传入ID的值,以及返回EventDetailVm

GetEventDetailQueryHandler.cs:返回查询

- 返回API的结构类似于
{"eventId": "123e4567-e89b-12d3-a456-426614174000","name": "Rock Concert","price": 100,"artist": "The Rock Band","date": "2023-12-25T20:00:00","description": "An amazing rock concert to end the year!","imageUrl": "https://example.com/images/rock-concert.jpg","categoryId": "456e7890-f12g-34h5-i678-901234567890","category": {"id": "456e7890-f12g-34h5-i678-901234567890","name": "Music"}
}
1.3 创建CreateEventCommand并使用validation
- 设置验证类
CreateEventCommandValidator.cs
using FluentValidation;
using GloboTicket.TicketManagement.Application.Contracts.Persistence;
using System;
using System.Threading;
using System.Threading.Tasks;namespace GloboTicket.TicketManagement.Application.Features.Events.Commands.CreateEvent
{public class CreateEventCommandValidator : AbstractValidator<CreateEventCommand>{private readonly IEventRepository _eventRepository;public CreateEventCommandValidator(IEventRepository eventRepository){_eventRepository = eventRepository;RuleFor(p => p.Name).NotEmpty().WithMessage("{PropertyName} is required.").NotNull().MaximumLength(50).WithMessage("{PropertyName} must not exceed 50 characters.");RuleFor(p => p.Date).NotEmpty().WithMessage("{PropertyName} is required.").NotNull().GreaterThan(DateTime.Now);RuleFor(e => e).MustAsync(EventNameAndDateUnique).WithMessage("An event with the same name and date already exists.");RuleFor(p => p.Price).NotEmpty().WithMessage("{PropertyName} is required.").GreaterThan(0);}private async Task<bool> EventNameAndDateUnique(CreateEventCommand e, CancellationToken token){return !(await _eventRepository.IsEventNameAndDateUnique(e.Name, e.Date));}}
}
- Command类:
CreateEventCommand.cs
using MediatR;namespace GloboTicket.TicketManagement.Application.Features.Events.Commands.CreateEvent
{public class CreateEventCommand: IRequest<Guid>{public string Name { get; set; } = string.Empty;public int Price { get; set; }public string? Artist { get; set; }public DateTime Date { get; set; }public string? Description { get; set; }public string? ImageUrl { get; set; }public Guid CategoryId { get; set; }public override string ToString(){return $"Event name: {Name}; Price: {Price}; By: {Artist}; On: {Date.ToShortDateString()}; Description: {Description}";}}
}
CreateEventCommandHandler.cs:处理Command,并且使用validator

- 自定义验证逻辑:查询在
IEventRepository接口里

2. EFcore层
-
数据库接口层:Core层的Contracts里的Persistence

-
实现层:Infrastructure层的Persistence

2.1 BaseRepository
BaseRepository.cs:定义

2.2 CategoryRepository
CategoryRepository.cs:继承BaseRepository,以及实现接口

2.3 OrderRepository
OrderRepository.cs使用分页

3. Email/Excel导出
3.1 Email
1. IEmail接口层

- 接口
namespace GloboTicket.TicketManagement.Application.Contracts.Infrastructure
{public interface IEmailService{Task<bool> SendEmail(Email email);}
}
2. Email的Model层
- Model实体:定义Email发送的内容和设置

3. 具体Email的实现层
- 在Infrastructure层里的infrastructure里实现

4. 配置settings
appsettings.json

3.2 Excel导出
1. 导出excel接口层
- Core文件夹/Application类库/Contracts文件夹/infrastructure文件夹/
IEmailService.cs
namespace GloboTicket.TicketManagement.Application.Contracts.Infrastructure
{public interface ICsvExporter{byte[] ExportEventsToCsv(List<EventExportDto> eventExportDtos);}
}
2. Controller层
- API文件夹/GloboTicket.TicketManagement.Api类库/Controllers文件夹/
EventsController.cs

3. Query层

GetEventsExportQuery.cs:返回值EventExportFileVm类,无参数
using MediatR;namespace GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport
{public class GetEventsExportQuery: IRequest<EventExportFileVm>{}
}
EventExportFileVm.cs:定义返回的文件类
public class EventExportFileVm
{public string EventExportFileName { get; set; } = string.Empty;public string ContentType { get; set; } = string.Empty;public byte[]? Data { get; set; }
}
EventExportDto.cs:
namespace GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport
{public class EventExportDto{public Guid EventId { get; set; }public string Name { get; set; } = string.Empty;public DateTime Date { get; set; }}
}
- handler

4. 实现IExcelService
- Infrastructure文件夹/Infrastructure类库/FileExport文件夹/
CsvExporter.cs
using CsvHelper;
using GloboTicket.TicketManagement.Application.Contracts.Infrastructure;
using GloboTicket.TicketManagement.Application.Features.Events.Queries.GetEventsExport;namespace GloboTicket.TicketManagement.Infrastructure.FileExport
{public class CsvExporter : ICsvExporter{public byte[] ExportEventsToCsv(List<EventExportDto> eventExportDtos){using var memoryStream = new MemoryStream();using (var streamWriter = new StreamWriter(memoryStream)){using var csvWriter = new CsvWriter(streamWriter);csvWriter.WriteRecords(eventExportDtos);}return memoryStream.ToArray();}}
}
4. 定义response/全局错误处理中间件
4.1 统一response
- 除了使用.net直接返回状态码之外,还可以统一响应的格式
{"success": true, //是否成功"message": "操作成功", //操作结果"data": {}, //返回数据内容"errorCode": null //错误类型或错误码
}
1. 定义统一的返回类
ApiResponse.cs类:处理所有返回的格式
public class ApiResponse<T>
{public bool Success { get; set; }public string Message { get; set; }public T? Data { get; set; }public string? ErrorCode { get; set; }public List<string>? ValidationErrors { get; set; }public ApiResponse(bool success, string message, T? data = default, string? errorCode = null){Success = success;Message = message;Data = data;ErrorCode = errorCode;}public static ApiResponse<T> SuccessResponse(T data, string message = "操作成功"){return new ApiResponse<T>(true, message, data);}public static ApiResponse<T> ErrorResponse(string message, string errorCode, List<string>? validationErrors = null){return new ApiResponse<T>(false, message, default, errorCode) { ValidationErrors = validationErrors };}
}
2. 使用
- 在Handler里使用
public async Task<ApiResponse<CreateCategoryDto>> Handle(CreateCategoryCommand request, CancellationToken cancellationToken)
{// 1. 初始化响应var validator = new CreateCategoryCommandValidator();var validationResult = await validator.ValidateAsync(request);// 2. 验证失败,返回错误响应if (validationResult.Errors.Count > 0){var validationErrors = validationResult.Errors.Select(e => e.ErrorMessage).ToList();return ApiResponse<CreateCategoryDto>.ErrorResponse("请求验证失败", "VALIDATION_ERROR", validationErrors);}// 3. 验证成功,继续处理业务逻辑var category = new Category() { Name = request.Name };category = await _categoryRepository.AddAsync(category);var categoryDto = _mapper.Map<CreateCategoryDto>(category);// 4. 返回成功响应return ApiResponse<CreateCategoryDto>.SuccessResponse(categoryDto, "分类创建成功");
}
- 成功返回:
{"success": true,"message": "分类创建成功","data": {"id": 1,"name": "Sport"}
}
- 验证失败
{"success": false,"message": "请求验证失败","errorCode": "VALIDATION_ERROR","validationErrors": ["分类名称不能为空","分类名称长度不能超过50个字符"]
}
4.2 全局错误处理中间件
5. 用户权限相关
5.1 用户权限相关的接口层
- Core文件夹/Application类库/Contracts文件夹/Identity文件夹

5.2 登录/注册/jwt 实体类定义
- Core文件夹/Application类库/Contracts文件夹/Models文件夹/Authentication文件夹

5.2 用户实体
- Infrastructure文件夹/GloboTicket.TicketManagement.Identity类库/Models文件夹

ApplicationUser.cs:用户实体
//用户实体
namespace Demo.Domain.Entities
{public class User{public Guid Id { get; set; } = Guid.NewGuid(); public string FirstName { get; set; } = null!;public string LastName { get; set; } = null!;public string Email { get; set; } = null!;public string Password { get; set; } = null!;public string Role { get; set; } = null!;}
}
5.3 用户认证所有接口实现的地方
- 用户登录注册以及jwt所有接口实现的地方

5.4 用户服务注册
- 所有Jwt和用户相关的服务注册
namespace GloboTicket.TicketManagement.Identity
{public static class IdentityServiceExtensions{public static void AddIdentityServices(this IServiceCollection services, IConfiguration configuration){services.Configure<JwtSettings>(configuration.GetSection("JwtSettings"));services.AddDbContext<GloboTicketIdentityDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("GloboTicketIdentityConnectionString"),b => b.MigrationsAssembly(typeof(GloboTicketIdentityDbContext).Assembly.FullName)));services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<GloboTicketIdentityDbContext>().AddDefaultTokenProviders();services.AddTransient<IAuthenticationService, AuthenticationService>();services.AddAuthentication(options =>{options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(o =>{o.RequireHttpsMetadata = false;o.SaveToken = false;o.TokenValidationParameters = new TokenValidationParameters{ValidateIssuerSigningKey = true,ValidateIssuer = true,ValidateAudience = true,ValidateLifetime = true,ClockSkew = TimeSpan.Zero,ValidIssuer = configuration["JwtSettings:Issuer"],ValidAudience = configuration["JwtSettings:Audience"],IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JwtSettings:Key"]))};o.Events = new JwtBearerEvents(){OnAuthenticationFailed = c =>{c.NoResult();c.Response.StatusCode = 500;c.Response.ContentType = "text/plain";return c.Response.WriteAsync(c.Exception.ToString());},OnChallenge = context =>{context.HandleResponse();context.Response.StatusCode = 401;context.Response.ContentType = "application/json";var result = JsonSerializer.Serialize("401 Not authorized");return context.Response.WriteAsync(result);},OnForbidden = context =>{context.Response.StatusCode = 403;context.Response.ContentType = "application/json";var result = JsonSerializer.Serialize("403 Not authorized");return context.Response.WriteAsync(result);}};});}}
}
6. 添加日志
7. 版本控制
8. 分页
9. 配置中间件和服务注册
- 模仿.ne5,将
Program.cs里注册分离
- 创建
StartupExtensions.cs用来将program.cs里的代码分离

- 在
program.cs里配置

二、测试
- 使用框架
Moq用来模拟数据
Shouldly 用来断言
xunit 测试框架
1. Unitest
- Automatically 代码片段测试,快速
- 测试的是Public API
- 独立运行 run in isolation
- 结果断言
2. Integration Tests
- end to end test between different layers
- more work to set up
- often linked with database
相关文章:
ASP.NET Core Clean Architecture
文章目录 项目地址一、项目主体1. CQRS1.1 Repository数据库接口1.2 GetEventDetail 完整的Query流程1.3 创建CreateEventCommand并使用validation 2. EFcore层2.1 BaseRepository2.2 CategoryRepository2.3 OrderRepository 3. Email/Excel导出3.1 Email1. IEmail接口层2. Ema…...
蓝桥杯备赛-精卫填海-DP
精卫终于快把东海填平了!只剩下了最后的一小片区域了。同时,西山上的木石也已经不多了。精卫能把东海填平吗? 事实上,东海未填平的区域还需要至少体积为 v 的木石才可以填平,而西山上的木石还剩下 n 块,每块…...
Windows10配置C++版本的Kafka,并进行发布和订阅测试
配置的环境为:Release x64下的环境 完整项目:https://gitee.com/jiajingong/kafka-publisher 1、首先下载相应的库文件(.lib,.dll) 参考链接: GitHub - eStreamSoftware/delphi-kafka GitHub - cloade…...
vue3 下载文件 responseType-blob 或者 a标签
在 Vue 3 中,你可以使用 axios 或 fetch 来下载文件,并将 responseType 设置为 blob 以处理二进制数据。以下是一个使用 axios 的示例: 使用 axios 下载文件 首先,确保你已经安装了 axios: npm install axios然后在你…...
【Gin-Web】Bluebell社区项目梳理6:限流策略-漏桶与令牌桶
本文目录 一、限流二、漏桶三、令牌桶算法四、Gin框架中实现令牌桶限流 一、限流 限流又称为流量控制,也就是流控,通常是指限制到达系统的并发请求数。 限流虽然会影响部分用户的使用体验,但是能一定程度上保证系统的稳定性,不至…...
51单片机-AT24CXX存储器工作原理
1、AT24CXX存储器工作原理 1.1、特点: 与400KHz,I2C总线兼容1.8到6.0伏工作电压范围低功耗CMOS技术写保护功能当WP为高电平时进入写保护状态页写缓冲器自定时擦写周期100万次编程/擦除周期可保存数据100年8脚DIP SOIC或TSSOP封装温度范围商业级和工业级…...
突破性能极限:DeepSeek开源FlashMLA解码内核技术解析
引言:大模型时代的推理加速革命 在生成式AI大行其道的今天,如何提升大语言模型的推理效率已成为行业焦点。DeepSeek团队最新开源的FlashMLA项目凭借其惊人的性能表现引发关注——在H800 GPU上实现580 TFLOPS计算性能,这正是大模型推理优化的…...
点击修改按钮图片显示有问题
问题可能出在表单数据的初始化上。在 ave-form.vue 中,我们需要处理一下从后端返回的图片数据,因为它们可能是 JSON 字符串格式。 vue:src/views/tools/fake-strategy/components/ave-form.vue// ... existing code ...Watch(value)watchValue(v: any) …...
[AI]从零开始的树莓派运行DeepSeek模型教程
一、前言 在前面的教程中,教了大家如何在windows中使用llama.cpp来运行DeepSeek模型。根据前面的教程中,我们也了解到了,我们只需要编译好llama.cpp就可以运行DeepSeek以及类似的LLM模型。那么本次教程就来教大家如何使用树莓派来运行大模型。…...
2024-2025 学年广东省职业院校技能大赛 “信息安全管理与评估”赛项 技能测试试卷(二)
2024-2025 学年广东省职业院校技能大赛 “信息安全管理与评估”赛项 技能测试试卷(二) 第一部分:网络平台搭建与设备安全防护任务书第二部分:网络安全事件响应、数字取证调查、应用程序安全任务书任务 1:应急响应&…...
Open WebUI本地部署教程
文章目录 1、系统环境配置2、源码下载2.1 github源码地址下载 3、环境启动3.1 后端环境3.2 前端环境 4、问题4.1 浏览器跨域问题4.2 all-MiniLM-L6-v2模型文件下载失败问题4.3 单独部署backend启动报错问题 1、系统环境配置 操作系统:windows/linux/macos Python版…...
Missing required prop: “maxlength“
背景: 封装一个使用功能相同使用频率较高的input公共组件作为子组件,大多数长度要求为200,且实时显示统计子数,部分input有输入提示。 代码实现如下: <template><el-input v-model"inputValue" t…...
dify本地部署
安装docker。 在官网安装docker。 如果遇到wsl报错,就使用 wsl --updata 进行更新。如果问题解决,进入docker应该是如下界面: 克隆 在自己创建的文件内使用 git clone gitgithub.com:langgenius/dify.git 或 git clone https://github.com…...
python学习一
学习网络安全为什么要学python? 1、在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工 具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者 编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定 的编程能力。 2、python是一门编程语言经常用它…...
git branch
文章目录 1.简介2.格式3.选项4.示例参考文献 1.简介 git branch 用于管理分支,包括查看、创建、删除、重命名和关联。 git branch 是 Git 版本控制系统中用于管理分支的命令。分支是 Git 的核心功能之一,允许开发者在同一个代码库中并行开发不同的功能…...
算法-图-数据结构(邻接矩阵)-BFS广度优先遍历
邻接矩阵广度优先遍历(BFS)是一种用于遍历或搜索图的算法,以下是具体介绍: 1. 基本概念 图是一种非线性的数据结构,由顶点和边组成,可分为无向图、有向图、加权图、无权图等。邻接矩阵是表示图的一种数…...
数学建模之数学模型—2:非线性规划
文章目录 非线性规划基本概念与结论凸集与凸函数极值条件无约束条件的极值判断条件有约束条件的极值判断条件 无约束非线性规划一维搜索算法步骤示例特点代码模板 最速下降法算法详细步骤 代码实现示例最优步长的求解 黄金分割法斐波那契法牛顿法阻尼牛顿法模式搜索法Powell方法…...
unity学习51:所有UI的父物体:canvas画布
目录 1 下载资源 1.1 在window / Asset store下下载一套免费的UI资源 1.2 下载,导入import 1.3 导入后在 project / Asset下面可以看到 2 画布canvas,UI的父物体 2.1 创建canvas 2.1.1 画布的下面是 event system是UI相关的事件系统 2.2 canvas…...
ctfshow做题笔记—栈溢出—pwn57~pwn60
目录 前言 一、pwn57(先了解一下简单的64位shellcode吧) 二、pwn58 三、pwn59(64位 无限制) 四、pwn60(入门难度shellcode) 前言 往前写了几道题,与shellcode有关,关于shellc…...
数据结构 1-2 线性表的链式存储-链表
1 原理 顺序表的缺点: 插入和删除移动大量元素数组的大小不好控制占用一大段连续的存储空间,造成很多碎片 链表规避了上述顺序表缺点 逻辑上相邻的两个元素在物理位置上不相邻 头结点 L:头指针 头指针:链表中第一个结点的存储…...
idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...
装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
GruntJS-前端自动化任务运行器从入门到实战
Grunt 完全指南:从入门到实战 一、Grunt 是什么? Grunt是一个基于 Node.js 的前端自动化任务运行器,主要用于自动化执行项目开发中重复性高的任务,例如文件压缩、代码编译、语法检查、单元测试、文件合并等。通过配置简洁的任务…...
MacOS下Homebrew国内镜像加速指南(2025最新国内镜像加速)
macos brew国内镜像加速方法 brew install 加速formula.jws.json下载慢加速 🍺 最新版brew安装慢到怀疑人生?别怕,教你轻松起飞! 最近Homebrew更新至最新版,每次执行 brew 命令时都会自动从官方地址 https://formulae.…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...
数据结构:递归的种类(Types of Recursion)
目录 尾递归(Tail Recursion) 什么是 Loop(循环)? 复杂度分析 头递归(Head Recursion) 树形递归(Tree Recursion) 线性递归(Linear Recursion)…...
算法—栈系列
一:删除字符串中的所有相邻重复项 class Solution { public:string removeDuplicates(string s) {stack<char> st;for(int i 0; i < s.size(); i){char target s[i];if(!st.empty() && target st.top())st.pop();elsest.push(s[i]);}string ret…...
