ASP.NET Core Web API GET 请求进阶(三)|Owner、Review、Reviewer 接口实战
作者:xiaoxin.gao · 2025-07-02 · 阅读时间:8分钟
1. 简介 在本系列的前两篇文章中,我们已经为 Pokemon、Category、Country 资源构建了丰 […]
文章目录
1. 简介
在本系列的前两篇文章中,我们已经为 Pokemon、Category、Country 资源构建了丰富的 GET 接口。本文将继续介绍如何为 Owner、Review、Reviewer 三个模型设计和实现多样的 GET 请求,涵盖列表查询、关联查询、存在性校验等常见场景,并额外讲解如何解决二对二/多对多关联导致的 JSON 循环引用问题。
2. 环境与项目结构回顾
- 技术栈:.NET 6+、C#、Entity Framework Core、AutoMapper
- 依赖注入:在
Program.cs中已注册DbContext、AutoMapper 及各仓储AddScoped < …, … >() -
项目结构:
/Interfaces
IOwnerRepository.cs
IReviewRepository.cs
IReviewerRepository.cs
…
/Repositories
OwnerRepository.cs
ReviewRepository.cs
ReviewerRepository.cs
…
/DTOs
OwnerDto.cs
ReviewDto.cs
ReviewerDto.cs
/Helpers
MappingProfiles.cs
/Controllers
OwnerController.cs
ReviewController.cs
ReviewerController.cs
/Data
AppDbContext.cs
3. Owner GET 接口实现
3.1 接口层(IOwnerRepository)
定义三种方法,满足关联查询与存在性校验:
public interface IOwnerRepository
{
IReadOnlyCollection < Pokemon > GetPokemonsByOwner(int ownerId); // 获取某 Owner 的所有 Pokemon
bool OwnerExists(int ownerId); // 判断 Owner 是否存在
}
3.2 仓储层(OwnerRepository)

在 Repositories/OwnerRepository.cs 中实现:
public class OwnerRepository : IOwnerRepository
{
private readonly AppDbContext _context;
public OwnerRepository(AppDbContext context) = > _context = context;
public IReadOnlyCollection < Pokemon > GetPokemonsByOwner(int ownerId) = >
_context.PokemonOwners
.Where(po = > po.Owner.Id == ownerId) // 过滤关联表
.Select(po = > po.Pokemon) // 投影到 Pokemon
.ToList();
public bool OwnerExists(int ownerId) = >
_context.Owners.Any(o = > o.Id == ownerId);
}
3.3 DTO 与映射(OwnerDto)

-
DTOs/OwnerDto.cs:public class OwnerDto { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } -
在
Helpers/MappingProfiles.cs中添加:CreateMap < Pokemon, PokemonDto > ();
3.4 控制器层(OwnerController)

[ApiController]
[Route("api/[controller]")]
public class OwnerController : ControllerBase
{
private readonly IOwnerRepository _repo;
private readonly IMapper _mapper;
public OwnerController(IOwnerRepository repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
// GET api/owner/{id}/pokemons
[HttpGet("{id}/pokemons")]
public ActionResult < IReadOnlyCollection < PokemonDto > > GetPokemonsByOwner(int id)
{
if (!_repo.OwnerExists(id))
return NotFound($"Owner {id} 未找到。");
var pokemons = _repo.GetPokemonsByOwner(id);
return Ok(_mapper.Map < IReadOnlyCollection < PokemonDto > > (pokemons));
}
}
4. Review GET 接口实现
4.1 接口层(IReviewRepository)

public interface IReviewRepository
{
IReadOnlyCollection < Review > GetReviews(); // 全部 Review
Review GetReview(int reviewId); // 单条 Review
IReadOnlyCollection < Review > GetReviewsByPokemon(int pokemonId); // 某 Pokemon 的所有 Review
bool ReviewExists(int reviewId); // Review 存在性校验
}
4.2 仓储层(ReviewRepository)

public class ReviewRepository : IReviewRepository
{
private readonly AppDbContext _context;
public ReviewRepository(AppDbContext context) = > _context = context;
public IReadOnlyCollection < Review > GetReviews() = >
_context.Reviews.ToList();
public Review GetReview(int reviewId) = >
_context.Reviews.FirstOrDefault(r = > r.Id == reviewId);
public IReadOnlyCollection < Review > GetReviewsByPokemon(int pokemonId) = >
_context.Reviews
.Where(r = > r.Pokemon.Id == pokemonId)
.ToList();
public bool ReviewExists(int reviewId) = >
_context.Reviews.Any(r = > r.Id == reviewId);
}
4.3 DTO 与映射(ReviewDto)

-
DTOs/ReviewDto.cs:public class ReviewDto { public int Id { get; set; } public string Title { get; set; } public string Text { get; set; } public int Rating { get; set; } } -
MappingProfiles中:CreateMap < Review, ReviewDto > ();
4.4 控制器层(ReviewController)
[ApiController]
[Route("api/[controller]")]
public class ReviewController : ControllerBase
{
private readonly IReviewRepository _repo;
private readonly IMapper _mapper;
public ReviewController(IReviewRepository repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
// GET api/review
[HttpGet]
public ActionResult < IReadOnlyCollection < ReviewDto > > GetReviews()
= > Ok(_mapper.Map < IReadOnlyCollection < ReviewDto > >(_repo.GetReviews()));
// GET api/review/{id}
[HttpGet("{id}")]
public ActionResult < ReviewDto > GetReview(int id)
{
if (!_repo.ReviewExists(id))
return NotFound($"Review {id} 未找到。");
return Ok(_mapper.Map < ReviewDto > (_repo.GetReview(id)));
}
// GET api/review/pokemon/{pokemonId}
[HttpGet("pokemon/{pokemonId}")]
public ActionResult < IReadOnlyCollection < ReviewDto > > GetReviewsByPokemon(int pokemonId)
{
var reviews = _repo.GetReviewsByPokemon(pokemonId);
return Ok(_mapper.Map < IReadOnlyCollection < ReviewDto > > (reviews));
}
}
5. Reviewer GET 接口实现
5.1 接口层(IReviewerRepository)
public interface IReviewerRepository
{
IReadOnlyCollection < Reviewer > GetReviewers(); // 全部 Reviewer
Reviewer GetReviewer(int reviewerId); // 单条 Reviewer
IReadOnlyCollection < Review > GetReviewsByReviewer(int reviewerId); // 某 Reviewer 的所有 Review
bool ReviewerExists(int reviewerId); // Reviewer 存在性校验
}
5.2 仓储层(ReviewerRepository)
public class ReviewerRepository : IReviewerRepository
{
private readonly AppDbContext _context;
public ReviewerRepository(AppDbContext context) = > _context = context;
public IReadOnlyCollection < Reviewer > GetReviewers() = >
_context.Reviewers.Include(r = > r.Reviews).ToList(); // Include 加载导航属性
public Reviewer GetReviewer(int id) = >
_context.Reviewers.FirstOrDefault(r = > r.Id == id);
public IReadOnlyCollection < Review > GetReviewsByReviewer(int id) = >
_context.Reviews.Where(r = > r.Reviewer.Id == id).ToList();
public bool ReviewerExists(int id) = >
_context.Reviewers.Any(r = > r.Id == id);
}
5.3 DTO 与映射(ReviewerDto)

-
DTOs/ReviewerDto.cs:public class ReviewerDto { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } -
MappingProfiles中:CreateMap < Reviewer, ReviewerDto > ();
5.4 控制器层(ReviewerController)

[ApiController]
[Route("api/[controller]")]
public class ReviewerController : ControllerBase
{
private readonly IReviewerRepository _repo;
private readonly IMapper _mapper;
public ReviewerController(IReviewerRepository repo, IMapper mapper)
{
_repo = repo;
_mapper = mapper;
}
// GET api/reviewer
[HttpGet]
public ActionResult < IReadOnlyCollection < ReviewerDto > > GetReviewers()
= > Ok(_mapper.Map < IReadOnlyCollection < ReviewerDto > >(_repo.GetReviewers()));
// GET api/reviewer/{id}
[HttpGet("{id}")]
public ActionResult < ReviewerDto > GetReviewer(int id)
{
if (!_repo.ReviewerExists(id))
return NotFound($"Reviewer {id} 未找到。");
return Ok(_mapper.Map < ReviewerDto > (_repo.GetReviewer(id)));
}
// GET api/reviewer/{id}/reviews
[HttpGet("{id}/reviews")]
public ActionResult < IReadOnlyCollection < ReviewDto > > GetReviewsByReviewer(int id)
{
var reviews = _repo.GetReviewsByReviewer(id);
return Ok(_mapper.Map < IReadOnlyCollection < ReviewDto > > (reviews));
}
}
6. JSON 循环引用问题处理

对于多对多或双向导航属性,默认序列化会出现循环依赖。解决方法:在 Program.cs 中替换默认 JSON 序列化设置为 ReferenceHandler.IgnoreCycles:
builder.Services.AddControllers()
.AddJsonOptions(options = >
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles);
7. 小结与最佳实践
- 接口优先:先定义
IRepository,再分层实现,保证松耦合与可测试性。 - DTO 限制:只暴露前端所需字段,杜绝敏感信息泄露。
- Include/Select:对导航属性显式加载,避免默认未加载导致 Null。
- 循环依赖:通过
ReferenceHandler.IgnoreCycles消除 JSON 循环引用。 - REST 设计:列表
/resource、详情/resource/{id}、子资源/resource/{id}/sub}。 - Swagger 测试:及时调试所有 GET 接口,验证状态码与返回数据。
通过本篇,你已完成 Owner、Review、Reviewer 三个模型的 GET 接口搭建,下一步将进入 POST/PUT/DELETE,实现完整的 CRUD 能力。期待与您下次再见!
原文引自YouTube视频:https://www.youtube.com/watch?v=FEanWuYq7us
热门推荐
一个账号试用1000+ API
助力AI无缝链接物理世界 · 无需多次注册
3000+提示词助力AI大模型
和专业工程师共享工作效率翻倍的秘密