如何在ASP.NET Core Web API中用仓储模式提交多对多关联数据
我们拥有Category、Post和PostCategory三个模型,希望采用仓储模式实现Post及其关联分类的POST请求方法,请问该如何创建对应的API?模型代码如下:
public class Category { public int Id { get; set; } public string Title { get; set; } public DateTime CreateDate { get; set; } = DateTime.Now; public ICollection<PostCategory> PostCategories { get; set; } } public class PostCategory { public int Id { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } public int PostsId { get; set; } public Posts Posts { get; set; } } public class Posts { public int Id { get; set; } public string Title { get; set; } public string Description { get; set; } public string ImageUrl { get; set; } public DateTime CreateDate { get; set; } = DateTime.Now; public Published IsPublished { get; set; } public ICollection<PostCategory> PostCategories { get; set; }=new HashSet<PostCategory>(); }
实现步骤
1. 定义仓储接口
先实现通用泛型仓储接口,再扩展Post专属的仓储方法:
// 通用仓储接口,封装基础CRUD操作 public interface IGenericRepository<T> where T : class { Task<T> AddAsync(T entity); Task SaveChangesAsync(); Task<List<T>> GetAllAsync(Expression<Func<T, bool>> filter = null); } // Post专属仓储接口,扩展关联分类的查询方法 public interface IPostRepository : IGenericRepository<Posts> { Task<Posts> GetPostWithCategoriesAsync(int postId); }
2. 实现仓储类
基于EF Core实现仓储逻辑,处理Post与PostCategory的关联:
public class GenericRepository<T> : IGenericRepository<T> where T : class { protected readonly DbContext _dbContext; protected readonly DbSet<T> _dbSet; public GenericRepository(DbContext dbContext) { _dbContext = dbContext; _dbSet = dbContext.Set<T>(); } public async Task<T> AddAsync(T entity) { await _dbSet.AddAsync(entity); return entity; } public async Task SaveChangesAsync() { await _dbContext.SaveChangesAsync(); } public async Task<List<T>> GetAllAsync(Expression<Func<T, bool>> filter = null) { return filter == null ? await _dbSet.ToListAsync() : await _dbSet.Where(filter).ToListAsync(); } } public class PostRepository : GenericRepository<Posts>, IPostRepository { public PostRepository(DbContext dbContext) : base(dbContext) { } public async Task<Posts> GetPostWithCategoriesAsync(int postId) { return await _dbSet .Include(p => p.PostCategories) .ThenInclude(pc => pc.Category) .FirstOrDefaultAsync(p => p.Id == postId); } }
3. 创建DTO(数据传输对象)
避免直接暴露实体模型,定义前后端交互的DTO:
// 前端提交Post的DTO public class CreatePostDto { [Required] [MaxLength(200)] public string Title { get; set; } public string Description { get; set; } public string ImageUrl { get; set; } public Published IsPublished { get; set; } // 前端传入需要关联的分类ID列表 public List<int> CategoryIds { get; set; } = new List<int>(); } // 返回给前端的Post详情DTO public class PostDetailDto { public int Id { get; set; } public string Title { get; set; } public string Description { get; set; } public string ImageUrl { get; set; } public DateTime CreateDate { get; set; } public Published IsPublished { get; set; } public List<CategoryDto> Categories { get; set; } } public class CategoryDto { public int Id { get; set; } public string Title { get; set; } }
4. 服务层处理业务逻辑
封装仓储操作,处理Post与分类的关联逻辑:
public class PostService { private readonly IPostRepository _postRepository; private readonly IGenericRepository<Category> _categoryRepository; public PostService(IPostRepository postRepository, IGenericRepository<Category> categoryRepository) { _postRepository = postRepository; _categoryRepository = categoryRepository; } public async Task<PostDetailDto> CreatePostAsync(CreatePostDto dto) { // 验证传入的分类是否存在 var existingCategoryIds = (await _categoryRepository.GetAllAsync(c => dto.CategoryIds.Contains(c.Id))) .Select(c => c.Id) .ToList(); var invalidCategoryIds = dto.CategoryIds.Except(existingCategoryIds).ToList(); if (invalidCategoryIds.Any()) { throw new ArgumentException($"分类ID {string.Join(",", invalidCategoryIds)} 不存在"); } // 构建Post实体 var post = new Posts { Title = dto.Title, Description = dto.Description, ImageUrl = dto.ImageUrl, IsPublished = dto.IsPublished }; // 添加关联的PostCategory记录 foreach (var categoryId in dto.CategoryIds) { post.PostCategories.Add(new PostCategory { CategoryId = categoryId }); } // 保存到数据库 await _postRepository.AddAsync(post); await _postRepository.SaveChangesAsync(); // 转换为DTO返回 return new PostDetailDto { Id = post.Id, Title = post.Title, Description = post.Description, ImageUrl = post.ImageUrl, CreateDate = post.CreateDate, IsPublished = post.IsPublished, Categories = (await _categoryRepository.GetAllAsync(c => dto.CategoryIds.Contains(c.Id))) .Select(c => new CategoryDto { Id = c.Id, Title = c.Title }) .ToList() }; } }
5. 注册依赖注入
在Program.cs中注册仓储与服务:
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>)); builder.Services.AddScoped<IPostRepository, PostRepository>(); builder.Services.AddScoped<PostService>();
6. 编写API控制器
实现POST接口处理请求:
[ApiController] [Route("api/posts")] public class PostsController : ControllerBase { private readonly PostService _postService; private readonly IPostRepository _postRepository; public PostsController(PostService postService, IPostRepository postRepository) { _postService = postService; _postRepository = postRepository; } [HttpPost] public async Task<ActionResult<PostDetailDto>> CreatePost([FromBody] CreatePostDto dto) { if (!ModelState.IsValid) { return BadRequest(ModelState); } try { var postDto = await _postService.CreatePostAsync(dto); return CreatedAtAction(nameof(GetPostById), new { id = postDto.Id }, postDto); } catch (ArgumentException ex) { return BadRequest(ex.Message); } } // 配套的GET接口,用于CreatedAtAction跳转 [HttpGet("{id}")] public async Task<ActionResult<PostDetailDto>> GetPostById(int id) { var post = await _postRepository.GetPostWithCategoriesAsync(id); if (post == null) { return NotFound(); } return new PostDetailDto { Id = post.Id, Title = post.Title, Description = post.Description, ImageUrl = post.ImageUrl, CreateDate = post.CreateDate, IsPublished = post.IsPublished, Categories = post.PostCategories.Select(pc => new CategoryDto { Id = pc.Category.Id, Title = pc.Category.Title }).ToList() }; } }
关键注意事项
- 事务处理:如果需要确保Post与PostCategory原子性保存,可在服务层使用
_dbContext.Database.BeginTransactionAsync()手动管理事务 - AutoMapper:DTO与实体的映射逻辑复杂时,可引入AutoMapper简化代码
- 异常处理:可自定义异常类型,配合全局异常过滤器统一返回错误格式
- 权限控制:根据业务需求添加接口权限验证逻辑
内容的提问来源于stack exchange,提问作者Santosh Vishwakarma
相关产品推荐
相关产品推荐

