1、创建 WebApi 项目



Program.cs 代码保留如下:
var builder = WebApplication.CreateBuilder(args); // Add services to the container.
var app = builder.Build(); // Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.Run();
2、Minimal APIs 最小 API 使用
在 Program.cs 中进行最小 API 使用:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
//获取所有衬衫数据列表
app.MapGet("/shirts", () => { return "获取所有衬衫数据列表"; });
//获取指定 ID 的衬衫数据
app.MapGet("/shirts/{id}", (int id) => { return $"获取 ID 为 {id} 的衬衫数据"; });
//创建一件新的衬衫
app.MapPost("/shirts", () => { return "创建一件新的衬衫"; });
//更新指定 ID 的衬衫数据
app.MapPut("/shirts/{id}", (int id) => { return $"更新 ID 为 {id} 的衬衫数据"; });
//删除指定 ID 的衬衫数据
app.MapDelete("/shirts/{id}", (int id) => { return $"删除 ID 为 {id} 的衬衫数据"; });
app.Run();
3、ASP.NET Core 中间件管道


app 中的 Use 开头的方法都是中间件组件使用方法。

4、Web API 控制器实现 Web API
方法 1:将路由写在方法前面并指定操作动词
using Microsoft.AspNetCore.Mvc;
namespace WebApiTest.Controllers
{
[ApiController]
public class ShirtsController : ControllerBase
{
//获取所有衬衫数据列表
[HttpGet]
[Route("api/shirts")]
public string GetShirts() { return "获取所有衬衫数据列表"; }
//获取指定 ID 的衬衫数据
[HttpGet]
[Route("api/shirts/{id}")]
public string GetShirtById(int id) { return $"获取 ID 为 {id} 的衬衫数据"; }
//创建一件新的衬衫
[HttpPost]
[Route("api/shirts")]
public string CreateShirt() { return "创建一件新的衬衫"; }
//更新指定 ID 的衬衫数据
[HttpPut]
[Route("api/shirts/{id}")]
public string UpdateShirt(int id) { return $"更新 ID 为 {id} 的衬衫数据"; }
[]
[]
{ ; }
}
}
方法 2:将路由写在类前面,再在方法前面指定操作动词
using Microsoft.AspNetCore.Mvc;
namespace WebApiTest.Controllers
{
[ApiController]
[Route("/api/[controller]")]
public class ShirtsController : ControllerBase
{
//获取所有衬衫数据列表
[HttpGet]
public string GetShirts() { return "获取所有衬衫数据列表"; }
//获取指定 ID 的衬衫数据
[HttpGet("{id}")]
public string GetShirtById(int id) { return $"获取 ID 为 {id} 的衬衫数据"; }
//创建一件新的衬衫
[HttpPost]
public string CreateShirt() { return "创建一件新的衬衫"; }
//更新指定 ID 的衬衫数据
[HttpPut("{id}")]
public string UpdateShirt(int id) { return $"更新 ID 为 {id} 的衬衫数据"; }
//删除指定 ID 的衬衫数据
[HttpDelete("{id}")]
() { ; }
}
}
5、基于控制器的 Web API 的路由
使用注解 [Route("/shirts")] 设置 Web API 的 URL,可以在方法前使用,也可以在类前使用。
方法前

类前

6、模型绑定,将 Http 请求中的数据映射到操作方法的参数
6.1 从路由绑定
在路由 Route("/shirts/{id}") 或在操作动词 HttpGet("{id}") 设置,在方法参数前设置 [FromRoute] 也可以省略。
[HttpGet("{id}")]
public string GetShirtById([FromRoute]int id) { return $"获取 ID 为 {id} 的衬衫数据"; }
6.2 从查询字符串中绑定
方法参数前使用注解 [FromQuery]。
http://localhost:5186/api/shirts/1?color=红色
[HttpGet("{id}")]
public string GetShirtById([FromRoute]int id, [FromQuery]string color) { return $"获取 ID 为 {id} 的衬衫数据"; }
6.3 从请求头 Header 中绑定
方法参数前使用注解 [FromHeader]。
[HttpGet("{id}")]
public string GetShirtById([FromRoute]int id, [FromQuery]string color, [FromHeader]int size) { return $"获取 ID 为 {id} 的衬衫数据,衬衫颜色为{color},大小为{size}"; }
6.4 从请求体 Body JSON 格式中绑定
方法参数前使用注解 [FromBody]。
[HttpPost]
public string CreateShirt([FromBody]Shirt shirt) { return $"创建一件新的衬衫,{shirt.Id}, {shirt.Name}, {shirt.Color}, {shirt.Gender}, {shirt.Price}"; }
6.5 从请求体 Body 表单中绑定
方法参数前使用注解 [FromForm]。
[HttpPost]
public string CreateShirt([FromForm]Shirt shirt) { return $"创建一件新的衬衫,{shirt.Id}, {shirt.Name}, {shirt.Color}, {shirt.Gender}, {shirt.Price}"; }
7、数据注解模型验证

8、ValidationAttribute 模型验证,继承 ValidationAttribute
创建脚本 Shirt_EnsureCorrectSizingAttribute.cs。
using System.ComponentModel.DataAnnotations;
using WebApiDemo.Models;
namespace WebApiTest.Models.Validations
{
public class Shirt_EnsureCorrectSizingAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var shirt = validationContext.ObjectInstance as Shirt;
if(shirt != null && !string.IsNullOrEmpty(shirt.Gender))
{
if(shirt.Gender.Equals("men", StringComparison.OrdinalIgnoreCase) && shirt.Size < 8)
{
return new ValidationResult("男性衬衫的尺码必须大于或等于 8。");
}
else if (shirt.Gender.Equals("women", StringComparison.OrdinalIgnoreCase) && shirt.Size < 6)
{
return new ValidationResult("女性衬衫的尺码必须大于或等于 6。");
}
}
return ValidationResult.Success;
}
}
}
在属性前添加注解

9、Web API 返回类型
返回类型使用 IActionResult。
正确返回使用 Ok(返回数据)。
未找到使用 NotFound()。
错误响应使用 BadRequest()。
[HttpGet("{id}")]
public IActionResult GetShirtById([FromRoute]int id, [FromQuery]string color, [FromHeader]int size)
{
if(id <= 0)
{
//错误响应
return BadRequest();
}
else if(id > 10)
{
//未找到
return NotFound();
}
//正确响应
return Ok($"获取 ID 为 {id} 的衬衫数据,衬衫颜色为{color},大小为{size}");
}
10、操作过滤器进行模型验证,继承 ActionFilterAttribute
用户输入传递的 id 可能不符合规范,可以通过操作过滤器进行模型验证对 id 进行验证。
创建脚本 Shirt_ValidateShirtIdFilterAttribute。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApiTest.Filters.ActionFilters
{
public class Shirt_ValidateShirtIdFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
var id = context.ActionArguments["id"] as int?;
if (id.HasValue)
{
if(id.Value <= 0)
{
context.ModelState.AddModelError("Id", "衬衫 ID 必须大于 0。");
var problemDetails = new Microsoft.AspNetCore.Mvc.ValidationProblemDetails(context.ModelState)
{
Status = 400,
Title = "请求参数错误",
};
context.Result = new BadRequestObjectResult(problemDetails);
}
}
}
}
}
需要使用验证的方法前添加注解

11、异常过滤器实现异常处理
更新衬衫之前可能其他请求把该衬衫已经删掉,更新时可能会报错,模拟该情景。
创建脚本 Shirt_HandleUpdateExceptionFilterAttribute。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApiTest.Filters.ExceptionFilters
{
public class Shirt_HandleUpdateExceptionFilterAttribute : ExceptionFilterAttribute
{
public int?[] shirtsId = new int?[] { 1, 2, 3, 4, 5 };
public override void OnException(ExceptionContext context)
{
base.OnException(context);
//判断衬衫 ID 是否存在 暂时假设 shirtsId 为数据库中已有的衬衫 ID 列表
var strShirtId = context.RouteData.Values["id"] as string;
if (int.TryParse(strShirtId, out int shirtId))
{
if (shirtsId.FirstOrDefault(x => x == shirtId) == null)
{
context.ModelState.AddModelError("Id", $"衬衫已经不存在");
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status404NotFound,
};
context.Result = new NotFoundObjectResult(problemDetails);
}
}
}
}
}
在需要异常处理的方法前使用

12、Web API 操作数据库
案例使用 SqlServer 数据库。
12.1 下载安装 Sql Server Developer Edition 和 SQL Server Management Studio

打开 SQL Server Management Studio 连接本地数据库

12.2 安装需要使用的包
- EntityFrameworkCore
- EntityFrameworkCore.Design
- EntityFrameworkCore.Tools
- EntityFrameworkCore.SqlServer
1)打开 管理 NuGet 程序包

2)搜索安装指定的包

3)双击查看是否安装成功


12.3 创建数据库上下文
1)创建脚本 ApplicationDbContext。
using Microsoft.EntityFrameworkCore;
using WebApiDemo.Models;
namespace WebApiTest.Data
{
public class ApplicationDbContext : DbContext
{
public DbSet<Shirt> Shirts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//数据播种
modelBuilder.Entity<Shirt>().HasData(
new Shirt { Id = 1, Name = "衬衫 1", Color = "red", Gender = "men", Size = 10, Price = 100 },
new Shirt { Id = 2, Name = "衬衫 2", Color = "blue", Gender = "men", Size = 12, Price = 140 },
new Shirt { Id = 3, Name = "衬衫 3", Color = "黑色", Gender = "women", Size = 11, Price = 132 },
new Shirt { Id = 4, Name = "衬衫 4", Color = "白色", Gender = "women", Size = 7, Price = 151 }
);
}
}
}
12.4 执行数据库迁移
1)添加连接字符串


2)获取连接字符串,视图->服务器资源管理器->数据连接





//需要将 master 替换为需要创建的数据库名称
Data Source=(local);Initial Catalog=master;Integrated Security=True;Trust Server Certificate=True
3)数据库上下文中创建构造函数指定连接数据库位置
using Microsoft.EntityFrameworkCore;
using WebApiDemo.Models;
namespace WebApiTest.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options) { }
public DbSet<Shirt> Shirts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//数据播种
modelBuilder.Entity<Shirt>().HasData(
new Shirt { Id = 1, Name = "衬衫 1", Color = "red", Gender = "men", Size = 10, Price = 100 },
new Shirt { Id = 2, Name = "衬衫 2", Color = "blue", Gender = "men", Size = 12, Price = 140 },
new Shirt { Id = 3, Name = "衬衫 3", Color = "黑色", Gender = "women", Size = 11, Price = 132 },
new Shirt { Id = , Name = , Color = , Gender = , Size = , Price = }
);
}
}
}
4)在 Program.cs 添加运行 EntityFrameworkCore 所需的服务

builder.Services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("ShirtStoreManagement")); });
5)创建迁移代码并进行迁移

控制台运行命令 Add-Migration Init(Init 可以替换为该次进行操作说明)

运行成功会创建在迁移文件夹下创建迁移代码

在控制台运行迁移代码,Update-Database Init(Init 为迁移代码名称,不指定会执行最新的代码)

执行成功可以在查看数据库创建成功

12.5 使用 EF Core 实现 Get 端点
在 ShirtsController 构造函数中依赖注入 Db 上下文

//获取所有衬衫数据列表
[HttpGet]
public IActionResult GetShirts()
{
return Ok(db.Shirts.ToList());
}
12.6 使用 EF Core 实现 Get by Id 端点
修改 Shirt_ValidateShirtIdFilterAttribute,实现验证 ID 在数据库是否存在。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApiTest.Data;
namespace WebApiTest.Filters.ActionFilters
{
public class Shirt_ValidateShirtIdFilterAttribute : ActionFilterAttribute
{
private readonly ApplicationDbContext db;
public Shirt_ValidateShirtIdFilterAttribute(ApplicationDbContext db) { this.db = db; }
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
var id = context.ActionArguments["id"] as int?;
if (id.HasValue)
{
if(id.Value <= 0)
{
context.ModelState.AddModelError("Id", "衬衫 ID 必须大于 0。");
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = 400,
Title = "请求参数错误",
};
context.Result = new BadRequestObjectResult(problemDetails);
}
else
{
var shirt = db.Shirts.Find(id.Value);
if(shirt == null)
{
context.ModelState.AddModelError(, );
problemDetails = ValidationProblemDetails(context.ModelState)
{
Status = ,
Title = ,
};
context.Result = NotFoundObjectResult(problemDetails);
}
{
context.HttpContext.Items[] = shirt;
}
}
}
}
}
}
在操作过滤器中查询数据库存在 ID 衬衫在 context.HttpContext.Items 中进行存在,然后在控制器中获取,避免重复进行数据库查询减少性能开销。
控制器查询代码
//获取指定 ID 的衬衫数据
[HttpGet("{id}")]
[TypeFilter(typeof(Shirt_ValidateShirtIdFilterAttribute))]
public IActionResult GetShirtById([FromRoute]int id)
{
//正确响应
return Ok(HttpContext.Items["shirt"]);
}
12.7 使用 EF Core 实现 Post 端点
//创建一件新的衬衫
[HttpPost]
public IActionResult CreateShirt([FromBody]Shirt shirt)
{
this.db.Shirts.Add(shirt);
this.db.SaveChanges();
return CreatedAtAction(nameof(GetShirtById), new { id = shirt.Id}, shirt);
}
12.8 使用 EF Core 实现 Put 端点
修改代码 Shirt_HandleUpdateExceptionFilterAttribute,实现验证当前 ID 数据是否还存在,可能存在当前操作之前数据被删除的可能。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApiTest.Data;
namespace WebApiTest.Filters.ExceptionFilters
{
public class Shirt_HandleUpdateExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly ApplicationDbContext db;
public Shirt_HandleUpdateExceptionFilterAttribute(ApplicationDbContext db) { this.db = db; }
public override void OnException(ExceptionContext context)
{
base.OnException(context);
//判断衬衫 ID 是否存在 暂时假设 shirtsId 为数据库中已有的衬衫 ID 列表
var strShirtId = context.RouteData.Values["id"] as string;
if (int.TryParse(strShirtId, out int shirtId))
{
if (this.db.Shirts.FirstOrDefault(x => x.Id == shirtId) == null)
{
context.ModelState.AddModelError("Id", $"衬衫已经在数据库不存在,可能在此之前被删除了");
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status404NotFound,
};
context.Result = new NotFoundObjectResult(problemDetails);
}
}
}
}
}
控制器更新代码
//更新指定 ID 的衬衫数据
[HttpPut("{id}")]
[TypeFilter(typeof(Shirt_ValidateShirtIdFilterAttribute))]
[TypeFilter(typeof(Shirt_HandleUpdateExceptionFilterAttribute))]
public IActionResult UpdateShirt(int id, [FromBody]Shirt shirt)
{
var shirtToUpdate = HttpContext.Items["shirt"] as Shirt;
shirtToUpdate.Name = shirt.Name;
shirtToUpdate.Size = shirt.Size;
shirtToUpdate.Color = shirt.Color;
shirtToUpdate.Gender = shirt.Gender;
shirtToUpdate.Price = shirt.Price;
this.db.SaveChanges();
return Ok($"ID 为{id}衬衫更新成功");
}
12.9 使用 EF Core 实现 Delete 端点
//删除指定 ID 的衬衫数据
[HttpDelete("{id}")]
[TypeFilter(typeof(Shirt_ValidateShirtIdFilterAttribute))]
public IActionResult DeleteShirt(int id)
{
var shirtToDelete = HttpContext.Items["shirt"] as Shirt;
this.db.Shirts.Remove(shirtToDelete);
this.db.SaveChanges();
return Ok(shirtToDelete);
}
13、Web Api 安全机制,使用 JWT 生成令牌和验证令牌

1)创建应用程序信息类,Application.cs
namespace WebApiDemo.Authority
{
public class Application
{
public int ApplicationId { get; set; }
public string? ApplicationName { get; set; }
public string? ClientId { get; set; }
public string? Secret { get; set; }
public string? Scopes { get; set; }
}
}
2)创建应用内容存储,AppRepository.cs,替代已存储数据库的应用程序注册信息
namespace WebApiDemo.Authority
{
public static class AppRepository
{
private static List<Application> _applications = new List<Application>
{
new Application
{
ApplicationId = 1,
ApplicationName = "MVCWebApp",
ClientId = "53D3C1E6-4587-4AD5-8C6E-A8E4BD59940E",
Secret = "0673FC70-0514-4011-B4A3-DF9BC03201BC",
Scopes = "read,write,delete"
}
};
public static Application? GetApplicationByClientId(string clientId)
{
return _applications.FirstOrDefault(app => app.ClientId == clientId);
}
}
}
3)创建应用凭证类,AppCredential.cs,用于接受认证请求参数
namespace WebApiDemo.Authority
{
public class AppCredential
{
public string? ClientId { get; set; } = string.Empty;
public string? Secret { get; set; } = string.Empty;
}
}
4)创建应用认证控制器接口,AuthorityController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using WebApiDemo.Authority;
namespace WebApiDemo.Controllers
{
[ApiController]
public class AuthorityController : ControllerBase
{
private readonly IConfiguration configuration;
public AuthorityController(IConfiguration configuration) { this.configuration = configuration; }
[HttpPost("auth")]
public IActionResult Authenticate([FromBody] AppCredential credential)
{
if (Authenticator.Authenticate(credential.ClientId, credential.Secret))
{
var expiresAt = DateTime.UtcNow.AddMinutes(10);
return Ok(new { access_token = Authenticator.CreateToken(credential.ClientId, expiresAt, configuration["SecurityKey"] ?? string.Empty), expires_at = expiresAt });
}
else
{
ModelState.AddModelError("Unauthorized", "未被授权");
var problemDetails = new ValidationProblemDetails(ModelState)
{
Status = StatusCodes.Status401Unauthorized,
};
return new UnauthorizedObjectResult(problemDetails);
}
}
}
}
5)设置密钥

6)创建应用认证身份验证逻辑类,Authenticator.cs
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
namespace WebApiDemo.Authority
{
public static class Authenticator
{
public static bool Authenticate(string clientId, string secret)
{
var app = AppRepository.GetApplicationByClientId(clientId);
if (app == null) return false;
return (app.ClientId == clientId && app.Secret == secret);
}
public static string CreateToken(string clientId, DateTime expiresAt, string strSecretKey)
{
//安全算法
//签名密钥
//负载(声明)
//生成签名
//算法
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(strSecretKey)),
SecurityAlgorithms.HmacSha256Signature);
//负载(声明)
var app = AppRepository.GetApplicationByClientId(clientId);
var claimsDictionary = new Dictionary<string, object>
{
{ , app?.ApplicationName??.Empty },
};
scopes = app?.Scopes?.Split() ?? Array.Empty<>();
(scopes.Length > )
{
( scope scopes)
{
claimsDictionary.Add(scope.Trim().ToLower(), );
}
}
tokenDescriptor = SecurityTokenDescriptor
{
SigningCredentials = signingCredentials,
Claims = claimsDictionary,
Expires = expiresAt,
NotBefore = DateTime.UtcNow,
};
tokenHandler = JsonWebTokenHandler();
tokenHandler.CreateToken(tokenDescriptor);
}
Task<IEnumerable<Claim>?> VerifyTokenAsync( tokenString, securityKey)
{
(.IsNullOrWhiteSpace(tokenString) || .IsNullOrWhiteSpace(securityKey))
{
;
}
keyBytes = System.Text.Encoding.UTF8.GetBytes(securityKey);
tokenHandle = JsonWebTokenHandler();
validationParameters = TokenValidationParameters
{
ValidateIssuer = ,
IssuerSigningKey = SymmetricSecurityKey(keyBytes),
ValidateIssuerSigningKey = ,
ValidateAudience = ,
ValidateLifetime = ,
ClockSkew = TimeSpan.Zero,
};
{
result = tokenHandle.ValidateTokenAsync(tokenString, validationParameters);
(result.SecurityToken != )
{
tokenObject = tokenHandle.ReadJsonWebToken(tokenString);
tokenObject.Claims ?? Enumerable.Empty<Claim>();
}
{
;
}
}
(SecurityTokenMalformedException)
{
;
}
(SecurityTokenExpiredException)
{
;
}
(SecurityTokenInvalidSignatureException)
{
;
}
(Exception)
{
;
}
}
}
}
7)通过过滤器方式实现 JWT 令牌验证
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApiDemo.Attributes;
using WebApiDemo.Authority;
namespace WebApiDemo.Filters.AuthFilters
{
public class JwtTokenAuthFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
//1. 从请求头中获取授权标识
if (!context.HttpContext.Request.Headers.TryGetValue("Authorization", out var authHeader))
{
context.Result = new UnauthorizedResult();
return;
}
string tokenString = authHeader.ToString();
//2. 去掉 Bearer 前缀
if (tokenString.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
tokenString = tokenString.Substring("Bearer ".Length).Trim();
}
else
{
context.Result = new UnauthorizedResult();
return;
}
//3. 获取配置和安全密钥
var configuration = context.HttpContext.RequestServices.GetService<IConfiguration>();
var securityKey = configuration?["SecurityKey"] ?? string.Empty;
//4. 验证令牌 并且 提取声明
//if(!await Authenticator.VerifyTokenAsync(tokenString, securityKey))
//{
claims = Authenticator.VerifyTokenAsync(tokenString, securityKey);
(claims == )
{
context.Result = UnauthorizedResult();
}
{
requiredClaism = context.ActionDescriptor.EndpointMetadata
.OfType<RequiredClaimAttribute>()
.ToList();
(requiredClaism != && !requiredClaism.All(rc => claims.Any(c => c.Type.Equals(rc.ClaimType, StringComparison.OrdinalIgnoreCase) && c.Value.Equals(rc.ClaimValue, StringComparison.OrdinalIgnoreCase))))
{
context.Result = StatusCodeResult();
}
}
}
}
}
8)授权、权限作用域校验类,RequiredClaimAttribute.cs
namespace WebApiDemo.Attributes
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,AllowMultiple = true)]
public class RequiredClaimAttribute : Attribute
{
public string ClaimType { get; }
public string ClaimValue { get; }
public RequiredClaimAttribute(string claimType, string claimValue)
{
this.ClaimType = claimType;
this.ClaimValue = claimValue;
}
}
}

