Skip to content
横幅:ASP.NET Core 面试题 Web API

ASP.NET Core 面试题 Web API ​

从 REST 基础到生产级 API 设计,一套完整的 ASP.NET Core Web API 知识体系。

在如今的开发环境中,Web API 已成为前后端分离架构的核心枢纽。无论是为 SPA 应用提供数据接口,还是构建微服务体系的通信基础,ASP.NET Core Web API 都是 .NET 开发者的必备技能。本文将系统性地梳理 Web API 的核心知识点,从 REST 理论基础到生产级最佳实践,一网打尽。


第一部分:REST 基础篇(必答送分题) ​

什么是 REST?如何在 ASP.NET Core 中设计 RESTful API? ​

REST 定义 ​

REST(Representational State Transfer,表述性状态转移) 是 Roy Fielding 在 2000 年博士论文中提出的一种分布式系统架构风格。它不是一个协议,而是一组设计约束。

REST 的六大约束:

约束说明
客户端-服务器关注点分离,客户端负责 UI,服务器负责数据存储和业务逻辑
无状态每个请求必须包含所有必要信息,服务器不保存客户端上下文
可缓存响应应标明是否可缓存,以提高性能
分层系统客户端不应关心它直接连接的是最终服务器还是中间代理
统一接口统一的资源操作方式(URI + HTTP 方法 + 状态码)
按需代码(可选)服务器可向客户端返回可执行代码(如 JavaScript)

RESTful API 设计原则 ​

1. 使用 HTTP 方法表达操作

HTTP 方法操作幂等性安全性说明
GET查询资源✅ 是✅ 是不应修改服务器状态
POST创建资源❌ 否❌ 否非幂等,多次创建会产生多个资源
PUT全量更新✅ 是❌ 否幂等,相同请求多次执行结果一致
PATCH部分更新❌ 否❌ 否非幂等(取决于实现),RFC 5789
DELETE删除资源✅ 是❌ 否幂等,删除不存在的资源返回 404

幂等性:相同请求重复执行多次,产生的结果相同。PUT 请求无论执行多少次,资源的最终状态一致。

2. URI 使用名词表示资源

✅ 好的设计
GET    /api/users              → 查询用户列表
GET    /api/users/{id}         → 查询单个用户
POST   /api/users              → 创建用户
PUT    /api/users/{id}         → 更新用户
DELETE /api/users/{id}         → 删除用户

❌ 不好的设计(RPC 风格)
GET    /api/GetUsers
POST   /api/CreateUser
POST   /api/UpdateUser

3. 使用合适的 HTTP 状态码

状态码含义使用场景
200 OK成功GET、PUT、PATCH 成功
201 Created资源已创建POST 成功创建资源,应返回 Location 头
204 No Content成功但无返回体DELETE 成功
400 Bad Request请求无效参数验证失败
401 Unauthorized未认证缺少或无效的认证凭证
403 Forbidden无权限已认证但无权访问
404 Not Found资源不存在URI 对应的资源不存在
409 Conflict资源冲突并发冲突、唯一键冲突
500 Internal Server Error服务器错误未预期的服务端异常

4. 无状态通信

每个请求必须包含处理所需的全部信息,服务器不保存任何客户端状态。认证信息(如 JWT)应在每个请求中携带。

5. HATEOAS(超媒体驱动,可选)

在响应中包含相关资源的链接,使客户端可以通过这些链接发现 API 能力,降低耦合度。

json
{
    "id": 1,
    "name": "Product A",
    "links": [
        { "rel": "self", "href": "/api/products/1" },
        { "rel": "reviews", "href": "/api/products/1/reviews" }
    ]
}

💡 面试金句

“REST 是一种架构风格而非协议,其核心是资源导向。我设计 RESTful API 时,会遵循:使用名词作为 URI、通过 HTTP 方法表达操作意图、合理使用状态码、保持无状态通信。在 ASP.NET Core 中,结合 [ApiController]、[Route] 特性和强类型 ActionResult<T> 可以很好地实现这些原则。”


第二部分:控制器与路由篇 ​

[ApiController] 特性及其优势 ​

[ApiController] 是 ASP.NET Core 2.1 引入的特性,专为 Web API 控制器设计,提供了一系列智能默认行为。强烈建议在所有 API 控制器上使用此特性。

csharp
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    // ...
}

主要优势 ​

1. 自动模型验证

控制器执行前自动检查 ModelState,验证失败时自动返回 400 Bad Request,无需手动编写 if (!ModelState.IsValid)。

csharp
// ❌ 不再需要这样写
[HttpPost]
public IActionResult Create(User user)
{
    if (!ModelState.IsValid)  // [ApiController] 已自动处理
    {
        return BadRequest(ModelState);
    }
    // ...
}

// ✅ [ApiController] 自动处理验证失败
[HttpPost]
public IActionResult Create(User user)
{
    // 如果验证失败,请求根本不会进入这个方法
}

2. 自动参数来源推断

根据参数类型自动推断绑定来源,减少冗余特性标注:

参数类型默认绑定来源示例
简单类型(int, string, bool 等)[FromQuery] 或 [FromRoute]int id
复杂类型(自定义类)[FromBody]User user
IFormFile[FromForm]IFormFile file
特殊类型(CancellationToken 等)框架自动注入CancellationToken ct
csharp
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // id → 从路由或查询字符串自动绑定
    [HttpGet("{id}")]
    public IActionResult Get(int id) { ... }
    
    // product → 从请求体 JSON 自动绑定
    [HttpPost]
    public IActionResult Create(Product product) { ... }
}

⚠️ 注意:自动推断也有陷阱——如果复杂类型同时从多个来源绑定,可能导致歧义。推荐在复杂场景中显式标注 [FromXxx] 特性。

3. 标准化错误响应

验证失败时返回 ProblemDetails 格式(RFC 7807),客户端可统一解析错误信息:

json
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "00-abc123...",
    "errors": {
        "Email": ["邮箱格式不正确"],
        "Age": ["年龄必须在 18~60 岁之间"]
    }
}

4. 多体参数限制

[ApiController] 会限制 [FromBody] 参数最多只能有一个,避免了传统 ASP.NET MVC 中多个复杂参数从同一个请求体绑定的混乱。

典型用法 ​

csharp
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpPost]
    public ActionResult<User> Create(User input)
    {
        // 验证失败会自动返回 400,代码更简洁
        return CreatedAtAction(nameof(GetUser), new { id = input.Id }, input);
    }

    [HttpGet("{id}")]
    public ActionResult<User> GetUser(int id)
    {
        var user = new User { Id = id, Name = "Alice" };
        return Ok(user);
    }
}

最佳实践:[ApiController] + ControllerBase + ActionResult<T> 是 ASP.NET Core Web API 的标准组合。


Web API 路由约定 ​

ASP.NET Core Web API 强烈推荐使用属性路由(Attribute Routing),而非传统集中式路由。

属性路由基础 ​

csharp
[ApiController]
[Route("api/[controller]")]  // [controller] 替换为控制器名(去掉 Controller 后缀)
public class ProductsController : ControllerBase
{
    [HttpGet]  // GET api/products
    public IActionResult GetAll() => Ok(new[] { new { Id = 1 } });
    
    [HttpGet("{id:int}")]  // GET api/products/5
    public IActionResult GetById(int id) => Ok(new { Id = id });
}

路由模板语法 ​

语法说明示例
{参数}基本占位符/products/{id} → /products/5
{参数:类型}类型约束/products/{id:int} → 仅匹配整数
{参数?}可选参数/products/{id?} → 可省略
{参数=默认值}默认值/products/{page=1} → 默认 1
{*参数}捕获所有/files/{*path} → 匹配剩余路径
[controller]控制器名占位符自动替换为 ProductsController → Products
[action]操作名占位符自动替换为方法名 GetById

可用约束类型 ​

约束说明示例
int整数{id:int}
bool布尔值{active:bool}
datetime日期时间{date:datetime}
decimal小数{price:decimal}
guidGUID{id:guid}
long长整数{id:long}
minlength(n)最小长度{name:minlength(3)}
maxlength(n)最大长度{name:maxlength(20)}
length(n,m)长度范围{name:length(3,20)}
regex(表达式)正则匹配{id:regex(^[a-zA-Z0-9]+$)}

传统路由(集中式) ​

csharp
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

对应的 URL:/Products/Get/5

选型建议:

  • Web API → 属性路由(灵活、直观、与资源绑定紧密)
  • 传统 MVC 页面 → 集中路由 + 属性路由混合
  • 大小写:路由匹配不区分大小写,但 REST 社区推荐统一使用小写 URI

API 版本管理 ​

API 版本管理允许 API 在演进时保持向后兼容。通过 Microsoft.AspNetCore.Mvc.Versioning 包实现。

安装:

bash
dotnet add package Microsoft.AspNetCore.Mvc.Versioning

配置 ​

csharp
services.AddApiVersioning(options =>
{
    options.ReportApiVersions = true;  // 响应头返回支持的版本
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    
    // 支持多种版本读取方式
    options.ApiVersionReader = ApiVersionReader.Combine(
        new HeaderApiVersionReader("X-API-Version"),
        new QueryStringApiVersionReader("api-version")
    );
});

四种版本管理方式 ​

方式示例特点
URL Path/api/v1/products最直观,推荐 REST API
Query String/api/products?api-version=1.0简单,URL 不变
HeaderX-API-Version: 1.0隐藏版本信息,URI 干净
Media TypeAccept: application/vnd.company.v1+json符合 REST 理念,但复杂

在控制器中使用 ​

csharp
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get(ApiVersion version) { ... }
}

// 或使用 [ApiVersion] 特性
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet, MapToApiVersion("1.0")]
    public IActionResult GetV1() { ... }
    
    [HttpGet, MapToApiVersion("2.0")]
    public IActionResult GetV2() { ... }
}

第三部分:数据交互篇 ​

内容协商(Content Negotiation) ​

内容协商让 API 根据客户端的 Accept 请求头返回不同格式的响应(JSON、XML 等)。

默认行为 ​

  • 默认仅支持 JSON(基于 System.Text.Json)
  • 客户端请求 Accept: application/json 时返回 JSON
  • 如果请求的格式不支持,默认返回 JSON(除非配置 ReturnHttpNotAcceptable)

启用 XML 支持 ​

csharp
services.AddControllers()
    .AddXmlSerializerFormatters();  // 使用 XmlSerializer
    // 或 .AddXmlDataContractSerializerFormatters();  // 使用 DataContractSerializer

配置严格内容协商 ​

csharp
services.AddControllers(options =>
{
    options.ReturnHttpNotAcceptable = true;  // 不支持时返回 406
});

请求示例 ​

GET /api/products/1
Accept: application/json    → 返回 JSON
Accept: application/xml     → 返回 XML
Accept: text/plain          → 返回 406(如果配置了严格模式)

选择序列化器 ​

序列化器使用方式特点
System.Text.Json默认性能好,.NET Core 3.0+ 默认
Newtonsoft.JsonAddNewtonsoftJson()功能丰富,兼容性好
XmlSerializerAddXmlSerializerFormatters()传统 XML 序列化器
DataContractSerializerAddXmlDataContractSerializerFormatters()支持 [DataContract] 特性控制

参数绑定:Body、Query、Route、Form ​

ASP.NET Core 支持从多种来源绑定参数,可通过特性显式指定:

csharp
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpPost("{id}")]
    public IActionResult Update(
        [FromRoute] int id,                    // 路由:/api/orders/5
        [FromQuery] string status,             // 查询字符串:?status=paid
        [FromBody] OrderUpdateRequest request, // 请求体 JSON
        [FromHeader] string authorization,     // 请求头:Authorization: Bearer xxx
        [FromForm] IFormFile file)             // 表单文件
    {
        // ...
    }
}

参数来源推断规则 ​

参数类型默认来源说明
简单类型(int, string, bool, DateTime 等)[FromQuery] / [FromRoute]优先从路由,其次查询字符串
复杂类型(自定义类)[FromBody]JSON 请求体
IFormFile[FromForm]表单文件上传
CancellationToken, HttpContext框架自动注入不需要指定来源

最佳实践:复杂场景下显式标注来源特性,代码可读性更高,避免歧义。


大文件上传与下载 ​

上传大文件 ​

小文件(< 28MB):使用 IFormFile

csharp
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
    // 文件已加载到内存/临时文件
    using var stream = file.OpenReadStream();
    // 处理...
}

大文件(GB 级):使用流式上传,避免内存溢出

csharp
[HttpPost("large-upload")]
[DisableFormValueModelBinding]  // 禁用模型绑定缓冲
public async Task<IActionResult> LargeUpload(CancellationToken ct)
{
    var request = HttpContext.Request;
    if (!request.HasFormContentType)
        return BadRequest("仅支持表单上传");

    var boundary = request.GetMultipartBoundary();
    var reader = new MultipartReader(boundary, request.Body);
    
    while (await reader.ReadNextSectionAsync(ct) is MultipartSection section)
    {
        if (string.IsNullOrEmpty(section.ContentDisposition))
            continue;
        
        using var stream = section.Body;
        // 流式处理:每次读取一块写入磁盘/云存储
        await ProcessStreamAsync(stream, ct);
    }
    
    return Ok();
}

注意事项:

  • 配置 FormOptions.MultipartBodyLengthLimit 控制最大上传大小
  • 禁用默认缓冲:避免整个文件加载到内存
  • 生产环境建议使用 分块上传 + 断点续传

下载大文件 ​

csharp
[HttpGet("download/{id}")]
public async Task<IActionResult> Download(string id)
{
    var filePath = Path.Combine("files", id);
    if (!System.IO.File.Exists(filePath))
        return NotFound();
    
    // FileStreamResult 流式返回,不加载到内存
    var stream = System.IO.File.OpenRead(filePath);
    var contentType = "application/octet-stream";
    return File(stream, contentType, Path.GetFileName(filePath));
}

最佳实践:

下载场景推荐方式说明
小文件(< 10MB)File(byte[], contentType, fileName)一次性加载到内存
大文件(> 10MB)File(Stream, contentType, fileName)流式写出
支持断点续传实现 Range 请求返回 206 Partial Content

第四部分:错误处理与状态码篇 ​

全局异常处理 ​

生产 vs 开发环境 ​

csharp
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();  // 开发环境:显示详细堆栈
}
else
{
    app.UseExceptionHandler("/error"); // 生产环境:返回友好错误
}

统一返回 ProblemDetails(RFC 7807) ​

csharp
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var feature = context.Features.Get<IExceptionHandlerFeature>();
        var ex = feature?.Error;

        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        context.Response.ContentType = "application/problem+json";

        var problem = new ProblemDetails
        {
            Status = 500,
            Title = "An unexpected error occurred",
            Detail = "Please contact support with the trace ID",
            Instance = context.Request.Path
        };
        problem.Extensions["traceId"] = context.TraceIdentifier;

        await context.Response.WriteAsJsonAsync(problem);
    });
});

返回示例:

json
{
    "type": "about:blank",
    "title": "An unexpected error occurred",
    "status": 500,
    "detail": "Please contact support with the trace ID",
    "instance": "/api/products",
    "traceId": "00-abc123def456-789"
}

位置建议:异常处理中间件应放在管道前部(UseRouting 之前),确保能捕获后续所有中间件的异常。


返回合适的 HTTP 状态码 ​

状态码方法使用场景
200 OKreturn Ok(data)GET 成功返回数据
201 Createdreturn CreatedAtAction(nameof(Get), new { id }, data)POST 创建成功,带 Location
204 No Contentreturn NoContent()PUT/DELETE 成功但无返回体
400 Bad Requestreturn BadRequest(message)参数验证失败
401 Unauthorized[Authorize] 自动触发未认证(缺少/无效令牌)
403 Forbidden[Authorize(Roles="...")] 自动触发已认证但无权限
404 Not Foundreturn NotFound()资源不存在
409 Conflictreturn Conflict()并发冲突、唯一键冲突
412 Precondition Failedreturn StatusCode(412)ETag/If-Match 不满足

标准方法速查 ​

csharp
// 200 OK
return Ok(result);

// 201 Created
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);

// 204 No Content
return NoContent();

// 400 Bad Request(自动包含验证错误)
return ValidationProblem(ModelState);
// 或
return BadRequest("参数无效");

// 404 Not Found
return NotFound();

// 409 Conflict
return Conflict(new { message = "资源名称已存在" });

// 412 Precondition Failed
return StatusCode(StatusCodes.Status412PreconditionFailed);

第五部分:类型选择与性能优化篇 ​

ActionResult<T> vs IActionResult ​

对比维度ActionResult<T>IActionResult
类型安全性✅ 强类型,返回类型明确❌ 弱类型,可返回任意内容
Swagger/OpenAPI✅ 自动推断返回类型⚠️ 需要 [Produces] 特性辅助
灵活性⚠️ 中等,适合单一类型✅ 高,适合多类型混合
可读性✅ 返回类型清晰⚠️ 类型不够明确
适用场景简单 CRUD API复杂逻辑、多种返回类型

示例对比 ​

csharp
// ActionResult<T>:类型明确,Swagger 自动生成 200 和 404
[HttpGet("{id}")]
public ActionResult<Product> Get(int id)
{
    var product = _service.Find(id);
    if (product == null) return NotFound();
    return Ok(product);
}

// IActionResult:需要额外特性标注返回类型
[HttpGet("{id}")]
[ProducesResponseType(typeof(Product), 200)]
[ProducesResponseType(404)]
public IActionResult Get(int id)
{
    var product = _service.Find(id);
    if (product == null) return NotFound();
    return Ok(product);
}

推荐原则:

  • 简单 CRUD API → ActionResult<T>(更清晰,文档更好)
  • 复杂场景(可能返回多种类型) → IActionResult(更灵活)

异步 API(async/await) ​

ASP.NET Core 对异步 I/O 提供了一等支持。使用 async/await 时,线程在等待 I/O 期间会被释放回线程池,从而提升服务器的并发吞吐能力。

csharp
[HttpGet("{id:int}")]
public async Task<ActionResult<Product>> GetAsync(int id, CancellationToken ct)
{
    // 数据库/HTTP/文件 I/O → 使用异步 API
    var product = await _repo.GetAsync(id, ct);
    if (product == null) return NotFound();
    return Ok(product);
}

核心原则 ​

原则说明
I/O 场景用异步数据库查询、HTTP 调用、文件读写 → 使用 async/await
CPU 场景用同步纯计算操作 → 使用同步方法
避免阻塞❌ 不要用 .Result、.Wait()、Task.Run() 包装 I/O
传递取消令牌将 CancellationToken 传递给所有可取消的操作

常见反模式 ​

csharp
// ❌ 反模式:同步阻塞异步方法
[HttpGet]
public IActionResult Get()
{
    var data = _repo.GetAsync().Result;  // 阻塞线程
    return Ok(data);
}

// ❌ 反模式:异步无 I/O(无意义)
[HttpGet]
public async Task<IActionResult> Get()
{
    var data = await Task.FromResult(ComputeData());  // CPU 计算包了异步壳
    return Ok(data);
}

CORS(跨域资源共享) ​

CORS 是浏览器安全策略的一部分,允许服务器声明哪些来源可以访问其资源。

配置示例 ​

csharp
// 1. 注册 CORS 服务
services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", builder =>
        builder.WithOrigins("https://frontend.com")  // 只允许指定域
               .AllowAnyHeader()
               .AllowAnyMethod()
               .AllowCredentials());  // 允许携带 Cookie
});

// 2. 启用 CORS
app.UseCors("AllowFrontend");

安全注意事项 ​

原则说明
最小授权只开放必要的域名、方法和头信息
禁止危险组合❌ 不能同时使用 .AllowAnyOrigin() 和 .AllowCredentials()(会运行时错误)
环境区分开发用宽松策略,生产用严格策略

限流(Rate Limiting) ​

.NET 8 之前的方案 ​

ASP.NET Core 无内置限流,常用第三方库:

  • AspNetCoreRateLimit:功能完善,支持 IP、ClientId 等策略
  • YARP / Azure API Management:网关层实现

.NET 8+ 内置方案 ​

csharp
// 配置限流
services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("fixed", opt =>
    {
        opt.PermitLimit = 5;               // 每窗口最多 5 次请求
        opt.Window = TimeSpan.FromSeconds(10);
        opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
        opt.QueueLimit = 2;                // 排队等待的请求数
    });
    
    options.AddSlidingWindowLimiter("sliding", opt =>
    {
        opt.PermitLimit = 10;
        opt.Window = TimeSpan.FromSeconds(30);
        opt.SegmentsPerWindow = 3;         // 滑动窗口段数
    });
});

// 启用限流中间件
app.UseRateLimiter();

限流策略类型:

策略说明适用场景
FixedWindowLimiter固定窗口,窗口期结束后重置简单限流,每 10 秒 5 次
SlidingWindowLimiter滑动窗口,更平滑需要更均匀的限流
TokenBucketLimiter令牌桶,允许突发允许短时间突发流量
ConcurrencyLimiter并发限制限制同时处理的请求数

第六部分:API 文档与测试篇 ​

Swagger / OpenAPI ​

使用 Swashbuckle.AspNetCore 或 NSwag 自动生成 OpenAPI 文档。

csharp
// 1. 安装:dotnet add package Swashbuckle.AspNetCore

// 2. 配置
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "示例 API 文档"
    });
    
    // 添加 JWT 认证支持
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme.",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.Http,
        Scheme = "bearer"
    });
});

// 3. 启用
app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
});

Web API 测试 ​

单元测试 ​

验证控制器逻辑,依赖项通过 Mock 替代。

csharp
[Fact]
public void Get_ValidId_ReturnsOk()
{
    // Arrange
    var mockRepo = new Mock<IProductRepository>();
    mockRepo.Setup(r => r.Get(1)).Returns(new Product { Id = 1, Name = "Book" });
    var controller = new ProductsController(mockRepo.Object);
    
    // Act
    var result = controller.Get(1) as OkObjectResult;
    
    // Assert
    Assert.NotNull(result);
    Assert.Equal(200, result.StatusCode);
    var product = result.Value as Product;
    Assert.Equal("Book", product?.Name);
}

集成测试 ​

使用 WebApplicationFactory 验证端到端行为。

csharp
public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/products");
        response.EnsureSuccessStatusCode();
        var body = await response.Content.ReadAsStringAsync();
        Assert.Contains("Book", body);
    }
}

测试最佳实践 ​

测试类型目标工具特点
单元测试验证业务逻辑xUnit + Moq快、隔离、无外部依赖
集成测试验证整体流程WebApplicationFactory接近真实环境,内存运行
端到端测试验证完整系统Testcontainers + 真实数据库最全面,耗时最长

命名约定:{MethodName}_{Scenario}_{ExpectedResult},如 GetUser_InvalidId_ReturnsNotFound


第七部分:安全篇 ​

API 认证与授权 ​

核心概念 ​

术语含义关键问题
Authentication(认证)验证“你是谁”提供凭证(用户名/密码、令牌、证书)
Authorization(授权)决定“你能做什么”基于身份或角色的访问控制

JWT Bearer 配置 ​

csharp
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = "your-issuer",
            ValidAudience = "your-audience",
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes("your-secret-key"))
        };
    });

app.UseAuthentication();
app.UseAuthorization();

端点保护 ​

csharp
// 要求登录
[Authorize]
public IActionResult Profile() { ... }

// 要求特定角色
[Authorize(Roles = "Admin")]
public IActionResult AdminPanel() { ... }

// 要求特定策略
[Authorize(Policy = "Scope.Read")]
public IActionResult GetData() { ... }

OAuth2 / OIDC 支持 ​

授权方式适用场景说明
授权码流 + PKCESPA / 移动应用最安全,推荐
Client Credentials服务间调用适用于后台 API 通信
隐式流传统 SPA已被授权码流替代

常见身份提供方:IdentityServer4/Duende、Azure AD(Entra ID)、Auth0、Okta


API 安全最佳实践 ​

HTTPS 强制 + HSTS ​

csharp
app.UseHttpsRedirection();
app.UseHsts();  // 仅生产环境启用

CORS 最小授权原则 ​

csharp
services.AddCors(o => o.AddPolicy("api", b =>
    b.WithOrigins("https://example.com")
     .WithMethods("GET", "POST", "PUT", "DELETE")
     .WithHeaders("Content-Type", "Authorization")
     .AllowCredentials()));  // 不能和 AllowAnyOrigin 同时使用

CSRF(跨站请求伪造) ​

  • JWT/Bearer 认证:一般不需要 CSRF 防护(令牌存储在 Header 中,不受 Cookie 自动发送影响)
  • Cookie 认证:需要启用 CSRF 防护
csharp
[ValidateAntiForgeryToken]  // 用于 Cookie 认证的 Action
public IActionResult Submit() { ... }

安全配置检查清单 ​

项目建议
传输安全生产环境强制 HTTPS + HSTS
认证使用 JWT Bearer 或 OAuth2/OIDC
授权基于角色/策略,最小权限原则
CORS只允许受信任来源,禁止 AllowAnyOrigin + AllowCredentials 组合
敏感数据不在日志、响应或 URL 中暴露敏感信息
输入验证使用数据注解验证,防止注入攻击

第八部分:数据映射与版本管理篇 ​

DTO 与对象映射(AutoMapper) ​

为什么使用 DTO ​

问题DTO 的解决方案
暴露数据库实体隐藏敏感字段(密码、内部 ID 等)
传输过多数据只返回客户端需要的字段
API 变更影响大实体变更不影响 API 合约
循环引用扁平化结构,避免循环引用问题

AutoMapper 使用示例 ​

csharp
// 1. 配置映射(通常在 Profile 中)
public class ProductProfile : Profile
{
    public ProductProfile()
    {
        CreateMap<Product, ProductDto>()
            .ForMember(dest => dest.FullName, 
                       opt => opt.MapFrom(src => src.Name + " (" + src.Code + ")"));
        CreateMap<CreateProductDto, Product>();
    }
}

// 2. 注册
services.AddAutoMapper(typeof(Program));

// 3. 使用
public class ProductsController : ControllerBase
{
    private readonly IMapper _mapper;
    
    [HttpGet]
    public ActionResult<IEnumerable<ProductDto>> Get()
    {
        var products = _productService.GetAll();
        return Ok(_mapper.Map<IEnumerable<ProductDto>>(products));
    }
}

替代方案 ​

方案适用场景说明
AutoMapper复杂映射、大量对象配置灵活,社区成熟
手动映射简单场景性能最好,最可控
Mapster高性能需求性能优于 AutoMapper,配置方式简洁
表达式树映射动态映射灵活但实现复杂

版本管理陷阱与兼容性 ​

主要陷阱 ​

陷阱后果解决方案
破坏性变更客户端报错新增版本而非修改
立即下线旧版本客户端无法升级提供过渡期,标记弃用
文档未更新客户端使用错误同步更新 Swagger/OpenAPI
客户端未升级调用失败提供迁移指南和版本通知

版本兼容性原则 ​

csharp
// 标记弃用
[ApiVersion("1.0")]
[ApiVersion("2.0", Deprecated = true)]  // 标记为弃用
public class ProductsController : ControllerBase
{
    // 响应头会返回 api-deprecated-versions: 2.0
}

最佳实践:

  1. 避免破坏性变更:不要修改已有字段的名称、类型或语义
  2. 合约变更需新版本:新增版本,保持旧版本可用
  3. 提供过渡期:旧版本标记弃用,给出下线时间
  4. 同步更新文档和客户端:Swagger 文档与版本保持一致

第九部分:并发处理篇 ​

乐观并发控制 ​

乐观并发控制假设并发冲突较少,通过版本号或时间戳检测数据是否被其他请求修改。

EF Core + RowVersion ​

csharp
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public byte[] RowVersion { get; set; }  // 必须为 byte[]
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.RowVersion)
        .IsRowVersion();  // 标记为并发令牌
}

// 更新时,EF Core 会在 WHERE 子句中包含 RowVersion
// 如果 RowVersion 不匹配,抛出 DbUpdateConcurrencyException
try
{
    await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
    // 处理并发冲突
    return Conflict("数据已被其他用户修改,请刷新后重试");
}

Web API + ETag ​

csharp
[HttpGet("{id}")]
public IActionResult Get(int id)
{
    var product = _service.Get(id);
    if (product == null) return NotFound();
    
    // 生成 ETag
    var etag = $"\"v1-{product.RowVersion}\"";
    Response.Headers.ETag = etag;
    return Ok(product);
}

[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] Product input)
{
    // 客户端发送 If-Match: "v1-xxx"
    var ifMatch = Request.Headers.IfMatch.ToString();
    // 验证 ETag 是否匹配
    if (!VerifyETag(id, ifMatch))
        return StatusCode(StatusCodes.Status412PreconditionFailed);
    
    // 执行更新...
}

并发处理状态码 ​

状态码场景
409 ConflictRowVersion 不匹配,并发冲突
412 Precondition FailedETag/If-Match 条件不满足
428 Precondition Required客户端必须提供条件头(RFC 6585)

第十部分:面试避坑清单 ​

序号❌ 常见错误✅ 正确理解
1认为 REST 是协议REST 是一种架构风格,不是协议
2GET 请求修改数据GET 应只读,不应产生副作用
3所有 API 返回 200 OK使用合适的 HTTP 状态码表达结果语义
4忘记加 [ApiController]API 控制器应始终使用此特性以获得默认行为
5忽略内容协商根据 Accept 头返回不同格式
6大文件上传到内存使用流式处理,避免 OOM
7不处理并发冲突使用 RowVersion/ETag 检测并发修改
8版本管理中直接修改旧版本旧版本应保留,新版本新增
9生产环境使用 UseDeveloperExceptionPage用 UseExceptionHandler 避免泄露敏感信息
10CORS 配置过于宽松遵循最小授权原则

小结 ​

ASP.NET Core Web API 是现代 .NET 开发的核心技能之一。掌握 REST 设计原则、[ApiController] 特性、路由、版本管理、安全策略以及并发处理,是构建高质量 API 的关键。

回顾全文,记住三个核心原则:

  1. 资源导向:使用名词 URI + HTTP 方法表达操作意图
  2. 状态码语义化:用合适的 HTTP 状态码准确表达响应结果
  3. 安全与性能并重:合理使用 CORS、限流、缓存和并发控制

Released under the MIT License.