(二)RestAPI 毛子(Tags——子实体/异常处理/验证/Search/Sort功能)
文章目录
- 项目地址
- 一、给Habit添加Tags
- 1.1 创建Tags
- 1. 创建一个新的HabitTags实体
- 2. 设置Habit和Tags的关系
- 3. 设置HabitTag表
- 4. 在HabitConfiguration里配置
- 5. 将表添加到EFCore里
- 6. 迁移数据
- 1.2 给Habit增加/修改标签
- 1. 创建UpsertHabitTagsDto
- 2. 创建查询HabitWithTagsDto
- 3. 创建HabitTagsController
- 1.3 使用Fluent API验证Create tag逻辑
- 1. 安装所需要的包
- 2. 注册服务
- 3. 添加CreateTagValidator
- 4. 使用validator
- 1.4 添加异常处理中间件
- 1. 创建全局异常处理中间件
- 2. 使用validator异常处理中间件
- 3. 注册服务以及中间件
- 4. 修改原来的validator
- 二、添加searching/filter功能
- 2.1 从GetHabit的Controller添加search功能
- 2.2 从GetHabit添加Filter
- 2.3 优化参数
- 三、动态排序
- 3.1 配置服务
- 3.2 创建sort服务
- 1. ISortMappingDefinition
- 2.SortMappingDefinition
- 3.SortMapping
- 4. SortMappingProvider
- 5. QueryableExtensions
- 3.3 在HabitMaping里添加具体需要排序的字段
- 3.4 在Program里注册服务
- 3.5 在Controller 使用服务GetHabits
- 3.6 发送请求
项目地址
- 教程作者:
- 教程地址:
- 代码仓库地址:
- 所用到的框架和插件:
dbt
airflow
一、给Habit添加Tags
1.1 创建Tags
1. 创建一个新的HabitTags实体
- 该实体需要创建一个表,关联Habits和Tags
namespace DevHabit.Api.Entities;public sealed class HabitTag
{public string HabitId { get; set; }public string TagId { get; set; }public DateTime CreatedAtUtc { get; set; }
}
2. 设置Habit和Tags的关系
- 在Habit 的是实体里创建关系
3. 设置HabitTag表
- 添加
HabitTagConfiguration.cs
配置文件
注意:这里Tag的WithMany()是空的
原因是,暂时我们业务上不考虑通过Tag来找所有的Habits; 但是,Habit里面,我们建立了多对多关系,需要通过habit来找到所有的Tag
4. 在HabitConfiguration里配置
- 修改了Habit的实体,我们需要在HabitConfiguration里配置关系
5. 将表添加到EFCore里
ApplicationDbContext.cs
里,添加新的表
public sealed class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
{public DbSet<Habit> Habits { get; set; }public DbSet<Tag> Tags { get; set; }public DbSet<HabitTag> HabitTags { get; set; }protected override void OnModelCreating(ModelBuilder modelBuilder){modelBuilder.HasDefaultSchema(Schemas.Application);modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);}
}
6. 迁移数据
- 在Api的Console里先输入
add-migration add HabitTag
- 换到docker环境,运行程序,自动执行
1.2 给Habit增加/修改标签
1. 创建UpsertHabitTagsDto
- 创建UpsertHabitTagsDto,用于处理前端传来的api数据
namespace DevHabit.Api.DTOs.HabitTags;public sealed record UpsertHabitTagsDto
{public required List<string> TagIds { get; init; }
}
2. 创建查询HabitWithTagsDto
- 用于查询单个habit后,显示该habit里的Tags
public sealed record HabitWithTagsDto
{public required string Id { get; init; }public required string Name { get; init; }public string? Description { get; init; }public required HabitType Type { get; init; }public required FrequencyDto Frequency { get; init; }public required TargetDto Target { get; init; }public required HabitStatus Status { get; init; }public required bool IsArchived { get; init; }public DateOnly? EndDate { get; init; }public MilestoneDto? Milestone { get; init; }public required DateTime CreatedAtUtc { get; init; }public DateTime? UpdatedAtUtc { get; init; }public DateTime? LastCompletedAtUtc { get; init; }public required string[] Tags { get; init; }
}
3. 创建HabitTagsController
- 该Controller用于给Habit 处理标签
[HttpPut]public async Task<ActionResult> UpsertHabitTags(string habitId, UpsertHabitTagsDto upsertHabitTagsDto){Habit? habit = await dbContext.Habits.Include(h => h.HabitTags).FirstOrDefaultAsync(h => h.Id == habitId);if (habit is null){return NotFound();}var currentTagIds = habit.HabitTags.Select(ht => ht.TagId).ToHashSet();if (currentTagIds.SetEquals(upsertHabitTagsDto.TagIds)){return NoContent();}List<string> existingTagIds = await dbContext.Tags.Where(t => upsertHabitTagsDto.TagIds.Contains(t.Id)).Select(t => t.Id).ToListAsync();if (existingTagIds.Count != upsertHabitTagsDto.TagIds.Count){return BadRequest("One or more tag IDs is invalid");}habit.HabitTags.RemoveAll(ht => !upsertHabitTagsDto.TagIds.Contains(ht.TagId));string[] tagIdsToAdd = upsertHabitTagsDto.TagIds.Except(currentTagIds).ToArray();habit.HabitTags.AddRange(tagIdsToAdd.Select(tagId => new HabitTag{HabitId = habitId,TagId = tagId,CreatedAtUtc = DateTime.UtcNow}));await dbContext.SaveChangesAsync();return NoContent();}
代码解释:
- 发起
http://localhost:5000/habits/h_0195e900-3c05-7ee6-8b26-7ec6b367d66a/tags
请求 - 接收一个
habitId
以及 UpsertHabitTagsDto
- 根据Id从数据库查询该habit的数据
- 查看是否有新增或者取消:通过数据库tags所有Id的hashSet和前端传来的进行对比,如果没有变化,则返回204
5. 排除非法habitId:查询Tags表的tagId和前端传来的Id进行对比,如果有不一样的,则报错
6. 删除Dto里取出的Tag,保存新增的Tags 完成upsert
7. 发送请求更改Tag
1.3 使用Fluent API验证Create tag逻辑
1. 安装所需要的包
2. 注册服务
3. 添加CreateTagValidator
4. 使用validator
1.4 添加异常处理中间件
1. 创建全局异常处理中间件
- 该中间件的作用是,捕获所有未处理的异常,返回标准化错误。
GlobalExceptionHandler.cs
namespace DevHabit.Api.Middleware;public sealed class GlobalExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler
{public ValueTask<bool> TryHandleAsync(HttpContext httpContext,Exception exception,CancellationToken cancellationToken){return problemDetailsService.TryWriteAsync(new ProblemDetailsContext{HttpContext = httpContext,Exception = exception,ProblemDetails = new ProblemDetails{Title = "Internal Server Error",Detail = "An error occurred while processing your request. Please try again"}});}
}
2. 使用validator异常处理中间件
- 不使用全局的validator,每个错误我们都需要if判断,以及每次需要传递错误信息,导致错误不统一以及大量重复代码
- 创建中间件,统一所有的validator返回格式,以及状态码
ValidationExceptionHandler.cs
public sealed class ValidationExceptionHandler(IProblemDetailsService problemDetailsService) : IExceptionHandler
{public async ValueTask<bool> TryHandleAsync(HttpContext httpContext,Exception exception,CancellationToken cancellationToken){if (exception is not ValidationException validationException){return false;}httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;var context = new ProblemDetailsContext{HttpContext = httpContext,Exception = exception,ProblemDetails = new ProblemDetails{Detail = "One or more validation errors occurred",Status = StatusCodes.Status400BadRequest}};var errors = validationException.Errors.GroupBy(e => e.PropertyName).ToDictionary(g => g.Key.ToLowerInvariant(),g => g.Select(e => e.ErrorMessage).ToArray());context.ProblemDetails.Extensions.Add("errors", errors);return await problemDetailsService.TryWriteAsync(context);}
}
3. 注册服务以及中间件
program.cs
4. 修改原来的validator
- 我们只需要验证即可,不需要在添加各种判断
二、添加searching/filter功能
2.1 从GetHabit的Controller添加search功能
- 对Name和Descripion进行匹配
- 发起请求
2.2 从GetHabit添加Filter
- 添加传入type类型和status
- 发起一个参数的请求:
http://localhost:5000/habits?type=1
- 发起多个参数的请求:
http://localhost:5000/habits?type=1&status=1
2.3 优化参数
- 上面的所有参数,我们都写在了controller里,如果参数多,会导致代码不好阅读
- 创建HabitsQueryParameters用来管理所有的Query
HabitsQueryParameters.cs
public sealed record HabitsQueryParameters
{[FromQuery(Name = "q")]public string? Search { get; set; }public HabitType? Type { get; init; }public HabitStatus? Status { get; init; }
}
- 修改Controller
三、动态排序
- 使用功能:传递
habits?sort=name desc,description
自动进行排序
3.1 配置服务
- 安装所需要的包
System.Linq.Dynamic.Core
- 在使用这个包的地方,添加该包引用
using System.Dynamic;
- 在 HabitsQueryParameters类里,添加sort字段
3.2 创建sort服务
1. ISortMappingDefinition
- 提供Sort服务的接口,用于注册
namespace DevHabit.Api.Services.Sorting;public interface ISortMappingDefinition;
2.SortMappingDefinition
- 接口实现:
public sealed class SortMappingDefinition<TSource, TDestination> : ISortMappingDefinition
{public required SortMapping[] Mappings { get; init; }
}
HabitMapping.cs
里实现:前端Dto转为实体
public static readonly SortMappingDefinition<HabitDto, Habit> SortMapping = new(){Mappings =[new SortMapping(nameof(HabitDto.Name), nameof(Habit.Name)),new SortMapping(nameof(HabitDto.Description), nameof(Habit.Description)),new SortMapping(nameof(HabitDto.Type), nameof(Habit.Type)),new SortMapping($"{nameof(HabitDto.Frequency)}.{nameof(FrequencyDto.Type)}",$"{nameof(Habit.Frequency)}.{nameof(Frequency.Type)}"),new SortMapping($"{nameof(HabitDto.Frequency)}.{nameof(FrequencyDto.TimesPerPeriod)}",$"{nameof(Habit.Frequency)}.{nameof(Frequency.TimesPerPeriod)}"),new SortMapping($"{nameof(HabitDto.Target)}.{nameof(TargetDto.Value)}",$"{nameof(Habit.Target)}.{nameof(Target.Value)}"),new SortMapping($"{nameof(HabitDto.Target)}.{nameof(TargetDto.Unit)}",$"{nameof(Habit.Target)}.{nameof(Target.Unit)}"),new SortMapping(nameof(HabitDto.Status), nameof(Habit.Status)),new SortMapping(nameof(HabitDto.EndDate), nameof(Habit.EndDate)),new SortMapping(nameof(HabitDto.CreatedAtUtc), nameof(Habit.CreatedAtUtc)),new SortMapping(nameof(HabitDto.UpdatedAtUtc), nameof(Habit.UpdatedAtUtc)),new SortMapping(nameof(HabitDto.LastCompletedAtUtc), nameof(Habit.LastCompletedAtUtc))]};
3.SortMapping
- 服务方法
public sealed record SortMapping(string SortField, string PropertyName, bool Reverse = false);
4. SortMappingProvider
- 该方法主要有两个功能:
- 从SortMappingDefinition里找到字段映射
- 传入一个排序字符串,比如 “name asc, price desc”,会提取出排序字段名(name, price),然后检查这些字段是否在 GetMappings 返回的字段映射中存在。
public sealed class SortMappingProvider(IEnumerable<ISortMappingDefinition> sortMappingDefinitions)
{public SortMapping[] GetMappings<TSource, TDestination>(){SortMappingDefinition<TSource, TDestination>? sortMappingDefinition = sortMappingDefinitions.OfType<SortMappingDefinition<TSource, TDestination>>().FirstOrDefault();if (sortMappingDefinition is null){throw new InvalidOperationException($"The mapping from '{typeof(TSource).Name}' into'{typeof(TDestination).Name} isn't defined");}return sortMappingDefinition.Mappings;}public bool ValidateMappings<TSource, TDestination>(string? sort){if (string.IsNullOrWhiteSpace(sort)){return true;}var sortFields = sort.Split(',').Select(f => f.Trim().Split(' ')[0]).Where(f => !string.IsNullOrWhiteSpace(f)).ToList();SortMapping[] mapping = GetMappings<TSource, TDestination>();return sortFields.All(f => mapping.Any(m => m.SortField.Equals(f, StringComparison.OrdinalIgnoreCase)));}
}
5. QueryableExtensions
- 使用扩展方法,可以让任何
Queryable<T>
调用ApplySort(...)
,并根据前端传来的排序字符串,结合后端配置的字段映射,对查询数据进行动态排序
using System.Linq.Dynamic.Core;namespace DevHabit.Api.Services.Sorting;internal static class QueryableExtensions
{public static IQueryable<T> ApplySort<T>(this IQueryable<T> query,string? sort,SortMapping[] mappings,string defaultOrderBy = "Id"){if (string.IsNullOrWhiteSpace(sort)){return query.OrderBy(defaultOrderBy);}string[] sortFields = sort.Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();var orderByParts = new List<string>();foreach (string field in sortFields){(string sortField, bool isDescending) = ParseSortField(field);SortMapping mapping = mappings.First(m =>m.SortField.Equals(sortField, StringComparison.OrdinalIgnoreCase));string direction = (isDescending, mapping.Reverse) switch{(false, false) => "ASC",(false, true) => "DESC",(true, false) => "DESC",(true, true) => "ASC"};orderByParts.Add($"{mapping.PropertyName} {direction}");}string orderBy = string.Join(",", orderByParts);return query.OrderBy(orderBy);}private static (string SortField, bool IsDescending) ParseSortField(string field){string[] parts = field.Split(' ');string sortField = parts[0];bool isDescending = parts.Length > 1 &&parts[1].Equals("desc", StringComparison.OrdinalIgnoreCase);return (sortField, isDescending);}
}
3.3 在HabitMaping里添加具体需要排序的字段
3.4 在Program里注册服务
3.5 在Controller 使用服务GetHabits
3.6 发送请求
habits?sort=name desc,description