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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
ASP.NET Core 实战入门/课时/综合练习
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综合练习

综合练习

端到端 Web API + EF + CI 小项目。

40 分钟

综合实战:迷你在线判题平台

这一课你将能做到

  1. 把前 11 课的模块组装成一个分层清晰的判题平台后端:提交 → 队列 → 判题 → 推送全链路可运行
  2. 用分层结构(Domain / Application / Infrastructure / Api)组织代码,说出每层与 Spring 项目的对应关系
  3. 拿到一份上线前检查清单,能独立完成「从空仓库到容器化部署」的完整交付

为什么要学

单点知识学完不等于会做系统——功夫在「接线」:谁持有谁、事务边界在哪、失败谁兜底。这一课把判题平台当验收考题:每处接线都对应前面某一课,含糊就回那课补。

人话:前 11 课是零件,这一课是总装车间。装完你就有一套「能写进简历、面试讲 20 分钟」的项目。

先建立图像

项目分层(Clean Architecture 精简版)与 Spring 项目对照:

OJ.Platform/
├─ src/
│  ├─ Oj.Domain/            实体与领域规则(无框架)  ≙ domain 模块
│  ├─ Oj.Application/       用例编排(接口+实现)     ≙ service 模块
│  ├─ Oj.Infrastructure/    出站适配:EF/Redis/MQ/沙箱 ≙ persistence+client
│  ├─ Oj.Api/               入站适配:REST + SignalR  ≙ web/controller
│  └─ Oj.Worker/            判题消费者(独立进程)    ≙ 消息监听应用
└─ OJ.Platform.sln

依赖方向:Api/Worker → Application → Domain;Infrastructure 实现 Application 的接口,由组合根装配。.NET 版六边形架构,与 Spring 分层治理同源。

跟着做

步骤 1:组合根——一个文件看全装配

// Oj.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);

// 数据(第 3 课)
builder.Services.AddDbContext<OjDbContext>(o =>
    o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

// 业务与 DI 生命周期(第 4 课)
builder.Services.AddScoped<SubmitUseCase>();
builder.Services.AddSingleton<IJudgeQueue, RabbitJudgeQueue>();   // 队列连接全局唯一
builder.Services.AddScoped<ProblemCacheService>();

// 缓存(第 6 课)
builder.Services.AddStackExchangeRedisCache(o =>
    o.Configuration = builder.Configuration.GetConnectionString("Redis"));

// 消息(第 9 课)
builder.Services.AddMassTransit(x => x.UsingRabbitMq((ctx, cfg) =>
{
    cfg.Host(builder.Configuration["RabbitMq:Host"]!);
    cfg.ConfigureEndpoints(ctx);
}));

// 实时/文档/全局异常(第 5、7 课)
builder.Services.AddSignalR();
builder.Services.AddSwaggerGen();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }

app.MapSubmissionEndpoints();   // /api/submissions
app.MapHub<JudgeHub>("/hubs/judge");
app.MapHealthChecks("/healthz");
app.Run();

public partial class Program { }   // 集成测试复用(第 8 课)

步骤 2:领域层——纯 C#,无框架痕迹

// Oj.Domain/Submission.cs
public class Submission
{
    public long Id { get; private set; }
    public long ProblemId { get; private set; }
    public string UserName { get; private set; } = null!;
    public string Language { get; private set; } = null!;
    public string Status { get; private set; } = "PENDING";   // PENDING→JUDGING→AC/WA/TLE/RE
    public int TimeMs { get; private set; }

    // 状态机收敛变更入口:非法流转直接拒绝(幂等第一道防线)
    public void MarkJudged(string verdict, int timeMs)
    {
        if (Status is not ("PENDING" or "JUDGING"))
            throw new InvalidOperationException($"状态 {Status} 不允许再判定");
        Status = verdict;
        TimeMs = timeMs;
    }
}

步骤 3:提交用例——事务与消息解耦

// Oj.Application/SubmitUseCase.cs
public class SubmitUseCase(OjDbContext db, IJudgeQueue queue)
{
    public async Task<SubmitResult> ExecuteAsync(SubmitDto dto, string user, CancellationToken ct)
    {
        if (!await db.Problems.AnyAsync(p => p.Id == dto.ProblemId, ct))
            return SubmitResult.NotFound();

        var submission = new Submission { ProblemId = dto.ProblemId, UserName = user, Language = dto.Language };
        db.Submissions.Add(submission);
        await db.SaveChangesAsync(ct);                 // ① 数据先落库(真相源)
        await queue.EnqueueAsync(new JudgeTask(submission.Id, dto.Language, dto.Code), ct); // ② 再投递
        return SubmitResult.Created(submission.Id);
    }
}

①②之间的原子性缺口由 Outbox(第 9 课)或「PENDING 超时补偿任务」兜底——面试主动讲出来,是体现深度的点。

步骤 4:判题 Worker——消费/判定/回写/推送

// Oj.Worker/JudgeWorker.cs
public class JudgeWorker(IServiceScopeFactory scopes, IJudgeQueue queue,
                         IHubContext<JudgeHub> hub, ILogger<JudgeWorker> log) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        var slots = new SemaphoreSlim(4);   // 沙箱并发上限
        while (await queue.WaitToReadAsync(ct))
        {
            var task = await queue.ReadAsync(ct);
            await slots.WaitAsync(ct);
            _ = ProcessAsync(task, slots, ct);
        }
    }

    private async Task ProcessAsync(JudgeTask task, SemaphoreSlim slots, CancellationToken ct)
    {
        try
        {
            using var scope = scopes.CreateScope();   // 后台无请求作用域(第 4 课)
            var db = scope.ServiceProvider.GetRequiredService<OjDbContext>();

            var outcome = await SandboxRunner.RunAsync(task.Code, task.Language, ct);
            var verdict = VerdictEvaluator.Evaluate(outcome.Matched, outcome.TimeMs, 2000, outcome.ExitCode);

            var submission = await db.Submissions.FirstAsync(s => s.Id == task.SubmissionId, ct);
            submission.MarkJudged(verdict, outcome.TimeMs);   // 幂等
            await db.SaveChangesAsync(ct);

            await hub.Clients.Group($"user-{submission.UserName}")   // 推送(第 7 课)
                     .SendAsync("judgeResult", new { task.SubmissionId, verdict, outcome.TimeMs }, ct);
        }
        catch (Exception ex)
        {
            log.LogError(ex, "判题失败 {SubmissionId}", task.SubmissionId);
            await queue.NackAsync(task);   // 重试/死信由队列层接管
        }
        finally { slots.Release(); }
    }
}

步骤 5:端到端验证

docker compose up -d   # 第 11 课的 compose:pg/redis/rabbitmq + api + worker
dotnet ef database update
curl -X POST localhost:8080/api/submissions -H "Content-Type: application/json" \
  -d '{"problemId":1,"language":"cpp","code":"int main(){return 0;}"}'   # → 201 PENDING
# 浏览器连 /hubs/judge → 秒级收到 {"verdict":"AC"}

步骤 6:上线前检查清单

  • 读接口 AsNoTracking + DTO 投影;提交防重(同用户同题 1 秒内拒绝)
  • 消费幂等(状态机)+ 重试有上限 + 死信有告警
  • 缓存 key 带版本号、空值短 TTL、写路径删缓存
  • SignalR backplane 已启用;推送失败不影响数据正确性
  • 结构化日志贯穿 traceId;/metrics /healthz 接入监控
  • 镜像非 root、密钥走环境变量/Secret、迁移走独立 Job
  • PENDING 超时补偿任务兜底(Worker 宕机的最后防线)

讲清楚要点

  1. 全链路的「真相源」是数据库:消息/缓存/SignalR 都是可重建的加速层,任何一个挂掉,重启后以数据库为准自愈(PENDING 重投、缓存回填、页面重拉)。
  2. 生命周期贯穿设计:Singleton(队列连接、判题引擎配置)→ Scoped(DbContext、用例)→ Transient(轻工具类);Worker 世界里没有请求,用 IServiceScopeFactory 手动造作用域。写错生命周期是本项目最高频 Bug。
  3. 横切关注点有明确归属:校验在 API 边界、业务规则在 Domain、外部访问在 Infrastructure、事务边界在 Application——对应 Spring「Controller 薄、Service 编排、Repository 持久化」。

你可能会懵的地方

  • 为什么判题 Worker 不做成 Web 应用的一部分? 资源特征不同(CPU 密集,独立扩容);安全边界不同(跑用户代码的机器能随时重建);故障隔离(沙箱崩溃不拖垮 API)。Spring 同理。

  • 为什么 SubmitUseCase 先 SaveChanges 再发消息,而不是事务包住? MQ 发送不能参与数据库事务,硬包是伪原子。正确姿势是 Outbox 或接受短暂不一致 + 补偿——经典的「双写问题」。

  • 测试怎么组织? Domain 全单测(VerdictEvaluator 分支);Application 用 NSubstitute 假依赖;Api 用 WebApplicationFactory 全链路(假沙箱);Worker 单测「重投同一消息」。金字塔照搬第 8 课。

  • 简历/面试怎么讲项目? 业务(削峰与实时性)→ 架构(消息解耦 + 实时推送)→ 深度点(幂等、Outbox、缓存三防)→ 数据(QPS、P99、积压告警),每点都能回溯到某一课。

错法对照

错法问题正解
Worker 复用 API 的 DbContext 单例并发判题线程安全灾难每任务 CreateScope 取独立 DbContext
状态字段直接 Status = "AC" 散落各处非法流转无法拦截领域方法 MarkJudged 收敛校验
只做推送不做超时补偿Worker 重启后提交永远 PENDING定时任务扫描超时 PENDING 重投
沙箱结果直接信前端回传用户可伪造 AC判定只在服务端沙箱完成后落库

卡点急救

  1. 消息与库状态打架(已 AC 又来一次):状态机拒绝非法流转 + 消费端先查状态,冲突消息记 Warning 后 ack 丢弃。
  2. 本地全链路起不来:compose 分层起(先 db/redis/mq,再 worker,最后 api),每层健康检查过再起下层;CI 用同 compose。
  3. 推送延迟高:别新建 scope 查库(用户组映射缓存在 Hub 内);大 payload 改传 ID 让前端回查。

面试常问

  • 这个系统最难的三个问题是什么? ①双写一致性 → Outbox + 补偿;②消费幂等 → 状态机 + Inbox;③资源隔离与公平 → 信号量并发池 + 按用户限流 + 超时熔断。

  • 如果流量增长 10 倍,先动哪里? 读路径先扩:缓存命中率与无状态服务加副本;判题瓶颈则加 Worker 副本(队列天然负载均衡);再考虑拆库、消息分区保序。

自测三问

  1. 为什么 Domain 层不允许引用 EF Core?
  2. 「数据库为准」在判题链路的哪些环节体现?
  3. 上线前清单哪三项缺失会直接造成资损?

小结一句话

总装的核心是依赖方向与真相源:领域不依赖框架、数据库是唯一事实、其余组件全是可重建的加速层——做到这两点,平台经得起流量与面试的拷问。

延伸

  • 官方教程:创建第一个 Web API
  • 课程完结:回到 /roadmaps/aspnet-core 复盘未掌握节点;继续深化可横向迁移本站《系统设计》《RabbitMQ》《Redis》课程。
加载本节练习…
← 上一课日志、任务与工具链