CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
ASP.NET Core 实战入门/课时/REST / GraphQL / gRPC
RawBlame
课时目录12 节

ASP.NET Core 实战入门

  1. 01ASP.NET Core 基础
  2. 02C# 与 .NET 基础
  3. 03Entity Framework Core
  4. 04依赖注入
  5. 05REST / GraphQL / gRPC
  6. 06数据存储与缓存
  7. 07SignalR 实时通信
  8. 08测试与 E2E
  9. 09消息队列
  10. 10微服务与网关
  11. 11日志、任务与工具链
  12. 12综合练习

REST / GraphQL / gRPC

HotChocolate、Grpc.Net、OData 与 OpenAPI。

35 分钟

构建 REST API:Minimal API 与 Controller

这一课你将能做到

  1. 用 Minimal API 和 Controller 两种形态实现同一组「提交代码」接口,并说出选型依据
  2. 完成模型绑定、参数校验、DTO 转换、分页返回的完整链路
  3. 用 IExceptionHandler 实现全局异常到 ProblemDetails 的统一映射(对应 @ControllerAdvice)

为什么要学

API 层是后端的第一道门面:绑定参数、校验输入、调用业务、映射状态码、描述文档。Spring 生态里这套由 @RestController + @Valid + @ControllerAdvice + Springdoc 组成;ASP.NET Core 8 把同样的能力拆成两套门面——Minimal API(轻、代码即路由)和 Controller(重约定、适合大团队),共享绑定/校验/文档底座。学完能按团队规模选形态,而不是背「哪个新用哪个」。

先建立图像

一次 POST 提交代码请求的完整旅程:

POST /api/submissions  {"problemId":1,"language":"cpp","code":"int main(){}"}
   │
   ├─ 路由匹配        MapPost / [HttpPost] 决定谁接单
   ├─ 模型绑定        JSON → SubmitDto(对应 @RequestBody)
   ├─ 模型校验        [Required] / @NotNull 不通过 → 自动 400
   ├─ 业务执行        注入的 JudgeService 干活(@Autowired)
   └─ 结果映射        201/400/404 + JSON 体(ResponseEntity 语义)

跟着做

步骤 1:Minimal API 版提交接口

public record SubmitDto(long ProblemId, string Language, string Code);
public record SubmissionView(long Id, string Status, DateTime CreatedAt);

var api = app.MapGroup("/api/submissions").WithTags("提交");

api.MapPost("/", async (SubmitDto dto, OjDbContext db, IJudgeQueue queue) =>
{
    var problem = await db.Problems.FindAsync(dto.ProblemId);
    if (problem is null) return Results.NotFound();

    var submission = new Submission
    {
        ProblemId = dto.ProblemId, Language = dto.Language,
        Status = "PENDING", CreatedAt = DateTime.UtcNow,
    };
    db.Submissions.Add(submission);
    await db.SaveChangesAsync();
    await queue.EnqueueAsync(submission.Id);   // 入判题队列(第 9 课)

    return Results.Created($"/api/submissions/{submission.Id}",
        new SubmissionView(submission.Id, "PENDING", submission.CreatedAt));
});

Results.Created(locationUrl, body) 返回 201 + Location 头,等价 ResponseEntity.created(uri).body(dto)。返回 IResult 而非裸对象:状态码显式可控。

步骤 2:等价 Controller 版

[ApiController]
[Route("api/submissions")]
public class SubmissionsController(OjDbContext db, IJudgeQueue queue) : ControllerBase
{
    [HttpPost]
    public async Task<ActionResult<SubmissionView>> Submit(SubmitDto dto)
    {
        var problem = await db.Problems.FindAsync(dto.ProblemId);
        if (problem is null) return NotFound();

        var submission = new Submission { ProblemId = dto.ProblemId, Language = dto.Language, Status = "PENDING" };
        db.Submissions.Add(submission);
        await db.SaveChangesAsync();
        await queue.EnqueueAsync(submission.Id);

        return Created($"/api/submissions/{submission.Id}",
            new SubmissionView(submission.Id, "PENDING", submission.CreatedAt));
    }
}
// Program.cs:AddControllers() + MapControllers()

步骤 3:校验(DataAnnotations,对应 Bean Validation)

using System.ComponentModel.DataAnnotations;

public record SubmitDto(
    [Range(1, long.MaxValue, ErrorMessage = "题目不存在")] long ProblemId,
    [Required, RegularExpression("^(cpp|java|python)$")] string Language,
    [Required, MaxLength(65536)] string Code);

[ApiController] 下校验失败自动返回 400 + ProblemDetails,方法体不会进——与 @Valid 抛 MethodArgumentNotValidException 的行为一致。Minimal API 需 AddValidation 过滤器或手动检查 ModelState。

步骤 4:分页查询接口(顺便看查询参数绑定)

api.MapGet("/", async (OjDbContext db, string? user, string? status,
    int page = 1, int size = 20, CancellationToken ct = default) =>
{
    var query = db.Submissions.AsNoTracking();
    if (user is not null)   query = query.Where(s => s.UserName == user);   // ≙ Specification
    if (status is not null) query = query.Where(s => s.Status == status);

    var total = await query.CountAsync(ct);
    var items = await query.OrderByDescending(s => s.CreatedAt)
        .Skip((page - 1) * size).Take(size)   // ≙ PageRequest.of(page, size)
        .Select(s => new SubmissionView(s.Id, s.Status, s.CreatedAt))
        .ToListAsync(ct);

    return Results.Ok(new { items, total, page, size });
});

方法签名的 string? user、int page = 1 自动从查询字符串绑定,等价 @RequestParam(required=false)。

步骤 5:全局异常处理(.NET 8 IExceptionHandler ≙ @ControllerAdvice)

public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception ex, CancellationToken ct)
    {
        logger.LogError(ex, "未处理异常: {Path}", ctx.Request.Path);

        var (status, title) = ex switch
        {
            KeyNotFoundException    => (StatusCodes.Status404NotFound, "资源不存在"),
            UnauthorizedAccessException => (StatusCodes.Status403Forbidden, "无权限"),
            _                       => (StatusCodes.Status500InternalServerError, "服务器内部错误"),
        };

        await Results.Problem(title: title, statusCode: status)
                     .ExecuteAsync(ctx);
        return true;   // true = 已处理,不再向上抛
    }
}

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();

所有未捕获异常统一变成 RFC 7807 application/problem+json,Spring 侧对应 @RestControllerAdvice + @ExceptionHandler。

步骤 6:OpenAPI 文档(对应 Springdoc/Swagger)

dotnet add package Swashbuckle.AspNetCore
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }  // /swagger

Minimal API 与 Controller 的接口都会进文档,WithSummary("...") 补充描述,配合 XML 注释。

讲清楚要点

  1. 形态选型是团队决策:单文件服务、原型选 Minimal API(启动快、样板少);接口多、多人协作选 Controller。两者可共存——新接口用 Minimal,存量留 Controller。
  2. DTO 与实体隔离是铁律:绝不能把 EF 实体直接当出入参(字段泄漏、循环引用、overposting)。用 record 定义 DTO,投影查询直接生成,等价 Java 的 DTO + MapStruct。
  3. 状态码要语义化:201+Location 表创建、204 表无返回体、409 表冲突、400 表校验失败。Results 覆盖全部语义,别一律 200。
  4. MapGroup 是 Minimal API 的模块化手段:分组统一加前缀、Tag、鉴权、限流,等价 Controller 类级注解的聚合。

你可能会懵的地方

  • Minimal API 怎么做「前置校验/日志」这类切面? Endpoint Filter(AddEndpointFilter),作用于路由命中后的端点,对应 Action Filter / Spring Interceptor:

    public class TimingFilter : IEndpointFilter
    {
        public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
        {
            var sw = Stopwatch.StartNew();
            var result = await next(ctx);
            Console.WriteLine($"耗时 {sw.ElapsedMilliseconds}ms");
            return result;
        }
    }
    
  • CancellationToken 为什么到处出现? 客户端断开/超时通过 HttpContext.RequestAborted 自动注入到参数,传给 EF 异步方法能省掉无效计算——等价 Spring MVC 的请求超时中断,但更显式。

错法对照

错法问题正解
接口直接返回 EF 实体循环引用、字段泄漏、overpostingrecord DTO + 投影查询
所有接口都返回 200 + 自定义 code 字段丢掉 HTTP 语义,网关/监控无法识别错误用状态码 + ProblemDetails
校验写在业务方法里逐个 if重复、遗漏、不可声明DataAnnotations / FluentValidation 统一声明
全局异常处理器里返回堆栈给前端泄漏内部信息生产环境只返回 title + traceId,日志里留堆栈

卡点急救

  1. POST 报 415:Content-Type 没给 application/json,或客户端发了表单。确认 header 与绑定来源特性。
  2. 校验不生效:Controller 忘了 [ApiController];Minimal API 需注册校验过滤器;record 的 required 成员构造失败先于校验,改用 DataAnnotations。
  3. Swagger 页面 404:确认对应环境执行了 UseSwagger/UseSwaggerUI(模板常只在 Development 注册),生产要开就移出条件块并加访问控制。

面试常问

  • Minimal API 和 Controller 怎么选?底层差异是什么? 底层共用 Endpoint Routing、模型绑定、DI、过滤器管线;差异在门面:Minimal API 用 lambda 注册,无反射扫描,冷启动更优;Controller 提供类级约定与过滤器生态。选型看规模与团队约定,性能差异已不是决定因素。

  • ProblemDetails 是什么,为什么推荐? RFC 7807 标准化的错误响应格式(type/title/status/detail/traceId),前端、网关、监控用统一契约读错误。.NET 8 通过 AddProblemDetails + IExceptionHandler 全链路自动生成,对应 Spring 6 的 ProblemDetail。

  • 如何防止 overposting(恶意绑定额外字段)? 入参用窄 DTO 只声明可写字段,实体绝不出现在绑定签名里;必要时 [BindNever] 排除敏感属性。与 Java DTO 隔离原则一致。

自测三问

  1. Results.Created 的两个参数各是什么语义?
  2. 请求里的 page 参数绑定失败(传了 "abc")会发生什么?
  3. IExceptionHandler 返回 true/false 分别代表什么?

小结一句话

API 层的核心资产是「绑定 → 校验 → 状态码 → 文档」四件套:Minimal API 与 Controller 只是门面之争,把 @Valid/@ControllerAdvice/ProblemDetails 心智平移即可。

延伸

  • 官方文档:Minimal API
  • 下一课:缓存与查询优化——Redis 分布式缓存、缓存三大经典问题,对照 Spring Cache。
加载本节练习…
← 上一课依赖注入下一课 →数据存储与缓存