ASP.NET Core Clean Architecture

news/2025/2/25 23:45:34

文章目录

  • 项目地址
  • 一、项目主体
    • 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.niftyadmin.cn/n/5866970.html

相关文章

C++的allactor

https://zhuanlan.zhihu.com/p/693267319 1 双层内存配置器 SGI设计了两层的配置器&#xff0c;也就是第一级配置器和第二级配置器。同时为了自由选择&#xff0c;STL又规定了 __USE_MALLOC 宏&#xff0c;如果它存在则直接调用第一级配置器&#xff0c;不然则直接调用第二级配…

华为数通 HCIP-Datacom H12-831 新题

2024年 HCIP-Datacom&#xff08;H12-831&#xff09;变题后的新题&#xff0c;完整题库请扫描上方二维码&#xff0c;新题在持续更新中。 某台IS-IS路由器自己生成的LSP信息如图所示&#xff0c;从LSP信息中不能推断出以下哪一结论? A&#xff1a;该路由器某一个接口的IPv6地…

本地VSCode远程连wsl2中的C++环境的开发配置指南

请参考上一遍文章&#xff1a;在windows上安装wsl2&#xff0c;在wsl2中配置C开发环境-CSDN博客

量子计算如何改变加密技术:颠覆与变革的前沿

量子计算如何改变加密技术:颠覆与变革的前沿 大家好,我是Echo_Wish,一名专注于人工智能和Python的自媒体创作者。今天,我们来探讨一个前沿且引人深思的话题——量子计算如何改变加密技术。随着量子计算的快速发展,传统的加密技术面临前所未有的挑战和机遇。本文将详细介绍…

算法(四)——动态规划

文章目录 基本思想适用条件最优子结构子问题重叠状态转移方程 解题步骤应用斐波那契数列背包问题最大子数组和 基本思想 动态规划的核心思想在于将一个复杂的问题分解为一系列相互关联的子问题&#xff0c;通过求解子问题并保存其解&#xff0c;避免对相同子问题的重复计算&am…

回合制游戏文字版(升级)

//在上一篇博客的基础上&#xff0c;加了细节的改动 //改动&#xff1a;添加了外貌&#xff0c;性别&#xff0c;招式的细节描绘&#xff1b;添加了个人信息展示界面 //一创建java文件1&#xff0c;命名为playGame package test2;import java.util.Random;public class play…

PHP入门基础学习四(PHP基本语法)

运算符 运算符&#xff0c;专门用于告诉程序执行特定运算或逻辑操作的符号。根据运算符的作用&#xff0c;可以将PHP语言中常见的运算符分为9类 算数运算符&#xff1a; 是用来处理加减乘除运算的符号 也是最简单和最常用的运算符号 赋值运算符 1. 是一个二元运算符&#x…

LabVIEW齿轮箱故障分析系统

在运维过程中&#xff0c;某大型风电场发现多台2.5MW风力发电机组在低速重载工况下频繁出现异常振动&#xff0c;导致齿轮箱温度升高和发电效率下降。传统的FFT频谱分析无法准确定位故障源&#xff0c;人工排查耗时且成本高昂。经初步检查&#xff0c;怀疑是行星齿轮箱内齿圈局…