You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 05:17:32