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

  1. 设置验证类 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));}}
}
  1. 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}";}}
}
  1. CreateEventCommandHandler.cs:处理Command,并且使用validator

在这里插入图片描述

  1. 自定义验证逻辑:查询在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里注册分离
  1. 创建StartupExtensions.cs用来将program.cs里的代码分离
    在这里插入图片描述
  2. 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/23831.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL数据库——表的约束

1.空属性&#xff08;null/not null&#xff09; 两个值&#xff1a;null&#xff08;默认的&#xff09;和not null&#xff08;不为空&#xff09; 数据库默认字段基本都是字段为空&#xff0c;但是实际开发时&#xff0c;尽可能保证字段不为空&#xff0c;因为数据为空没办法…

DeepSeek-R1:通过强化学习激发大语言模型的推理能力

注&#xff1a;此文章内容均节选自充电了么创始人&#xff0c;CEO兼CTO陈敬雷老师的新书《自然语言处理原理与实战》&#xff08;人工智能科学与技术丛书&#xff09;【陈敬雷编著】【清华大学出版社】 文章目录 DeepSeek大模型技术系列三DeepSeek大模型技术系列三》DeepSeek-…

蓝桥杯备考:递归初阶之汉诺塔问题

我们只要想一个主问题&#xff0c;我们是先把a上面n-1个盘子放在c里&#xff0c;然后再把第n个盘子放在b上&#xff0c;再利用a把c上n-1个盘子都放在b上就行了 #include <iostream> using namespace std;void dfs(int n,char x,char y,char z) {if(n0) return;dfs(n-1,x…

聊一聊vue如何实现角色权限的控制的

大家好&#xff0c;我是G探险者。 关于角色与权限控制&#xff0c;通常是分为两大类&#xff1a;一种是菜单权限&#xff1b;一种是操作权限。 菜单权限是指&#xff0c;每个角色对应着可以看到哪些菜单&#xff0c;至于每个菜单里面的每个按钮&#xff0c;比如增删改查等等这类…

如何将公钥正确添加到服务器的 authorized_keys 文件中以实现免密码 SSH 登录

1. 下载密钥文件 2. RSA 解析 将 id_ed25519 类型的私钥转换为 RSA 类型&#xff0c;要将 ED25519 私钥转换为 RSA 私钥&#xff0c;需要重新生成一个新的 RSA 密钥对。 步骤&#xff1a; 生成新的 RSA 密钥对 使用 ssh-keygen 来生成一个新的 RSA 密钥对。比如&#xff0c;执…

RK Android11 WiFi模组 AIC8800 驱动移植流程

RK Android WiFi模组 AIC8800 驱动移植流程 作者&#xff1a;Witheart更新时间&#xff1a;20250220 概要&#xff1a;本文介绍了基于 AIC8800D40 芯片的 WiFi6 模组 BL-M8800DS2-40 在 RK3568 平台上的驱动移植流程。主要涉及环境搭建、驱动代码分析、设备树修改、驱动编译配…

力扣3102.最小化曼哈顿距离

力扣3102.最小化曼哈顿距离 题目 题目解析及思路 题目要求返回移除一个点后的最小的最大曼哈顿距离 最大最小值的题一般直接想到二分 本题有一个简单办法就是利用切比雪夫距离 当正方形转45&#xff0c;即边上点**( x , y ) -> (x y , y - x)时&#xff0c;两点间max(…

BUUCTF--[极客大挑战 2019]RCE ME

目录 URL编码取反绕过 异或绕过 异或的代码 flag 借助蚁剑中的插件进行绕过 利用动态链接库 编写恶意c语言代码 进行编译 然后再写一个php文件 将这两个文件上传到/var/tmp下 运行payload 直接看代码 <?php error_reporting(0); if(isset($_GET[code])){$code$_G…

Linux----线程

一、基础概念对比 特性进程 (Process)线程 (Thread)资源分配资源分配的基本单位&#xff08;独立地址空间&#xff09;共享进程资源调度单位操作系统调度单位CPU调度的最小单位创建开销高&#xff08;需复制父进程资源&#xff09;低&#xff08;共享进程资源&#xff09;通信…

Missing required prop: “maxlength“

背景&#xff1a; 封装一个使用功能相同使用频率较高的input公共组件作为子组件&#xff0c;大多数长度要求为200&#xff0c;且实时显示统计子数&#xff0c;部分input有输入提示。 代码实现如下&#xff1a; <template><el-input v-model"inputValue" t…

DeepSeek引领目标检测新趋势:如何通过知识蒸馏优化模型性能

目录 一、知识蒸馏是什么&#xff1f; 二、知识蒸馏在目标检测中的重要性 提升实时性 跨任务迁移学习 三、如何使用知识蒸馏优化目标检测&#xff1f; 训练教师模型 生成软标签 训练学生模型 调节温度参数 多教师蒸馏&#xff08;可选&#xff09; 四、案例分享 定…

给老系统做个安全检查——Burp SqlMap扫描注入漏洞

背景 在AI技术突飞猛进的今天&#xff0c;类似Cursor之类的工具已经能写出堪比大部分程序员水平的代码了。然而&#xff0c;在我们的代码世界里&#xff0c;仍然有不少"老骥伏枥"的系统在兢兢业业地发光发热。这些祖传系统的代码可能早已过时&#xff0c;架构可能岌…

Pytorch实现论文:基于多尺度融合生成对抗网络的水下图像增强

简介 简介:提出了一种新型的水下图像增强算法,基于多尺度融合生成对抗网络,名为UMSGAN,以解决低对比度和颜色失真的问题。首先经过亮度的处理,将处理后的图像输入设计的MFFEM模块和RM模块生成图像。该算法旨在适应各种水下场景,提供颜色校正和细节增强。 论文题目:Und…

C++ DAY4

作业 代码 class Data { private:int a;int b; public://构造函数Data(int a0,int b0):a(a),b(b){}//set接口void setA(int index0){aindex;}void setB(int index0){bindex;}//get接口int getA(){return a;}int getB(){return b;}void show(){ cout <<"a " &…

Flutter 实现抖音风格底部导航栏

在移动应用开发中&#xff0c;良好的导航设计对用户体验至关重要。抖音作为一款现象级应用&#xff0c;其底部导航设计简洁直观&#xff0c;极具吸引力。本文将详细介绍如何使用 Flutter 开发一个类似抖音风格的底部导航栏&#xff0c;帮助开发者打造兼具美观与实用的导航界面。…

深入剖析:基于红黑树实现自定义 map 和 set 容器

&#x1f31f; 快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。&#x1f31f; 在 C 标准模板库&#xff08;STL&#xff09;的大家庭里&#xff0c;map和set可是超级重要的关联容器成员呢&#x1f60e;&#x…

前端面试题之HTML篇

1.src和href的区别 src用于替换当前元素&#xff0c;href用于在当前文档和引用资源之间确立联系。 src可用于img、input、style、script、iframe---同步加载执行 href可用于link、a---异步 1.用途不同 src 用于引入外部资源&#xff0c;通常是图像、视频、JavaScript 文件等&am…

硬件工程师入门教程

1.欧姆定律 测电压并联使用万用表测电流串联使用万用表&#xff0c;红入黑出 2.电阻的阻值识别 直插电阻 贴片电阻 3.电阻的功率 4.电阻的限流作用 限流电阻阻值的计算 单位换算关系 5.电阻的分流功能 6.电阻的分压功能 7.电容 电容简单来说是两块不连通的导体加上中间的绝…

01背包之---应用篇

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、01背包之---背包是否能被装满&#xff1f;例题1.分析题意例题2.分析题意 二、01背包之---装满背包有多少种组合?例题1.分析题意 三、01背包之---容量为N的…

DeepSeek赋能智慧文旅:新一代解决方案,重构文旅发展的底层逻辑

DeepSeek作为一款前沿的人工智能大模型&#xff0c;凭借其强大的多模态理解、知识推理和内容生成能力&#xff0c;正在重构文旅产业的发展逻辑&#xff0c;推动行业从传统的经验驱动向数据驱动、从人力密集型向智能协同型转变。 一、智能服务重构&#xff1a;打造全域感知的智…