【WebApi】C#创建WebApi学习
1、创建WebApi项目



Prgram.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、Minmal APIs最小API使用
Prgram.cs中进行最小API使用
var builder = WebApplication.CreateBuilder(args); // Add services to the container. var app = builder.Build(); // Configure the HTTP request pipeline. 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} 的衬衫数据"; } //删除指定ID的衬衫数据 [HttpDelete] [Route("api/shirts/{id}")] public string DeleteShirt(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}")] public string DeleteShirt(int id) { return $"删除ID为 {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、ValidationAtteibute模型验证,继承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 = "wemen", Size = 11, Price = 132 }, new Shirt { Id = 4, Name = "衬衫4", Color = "白色", Gender = "wemen", Size = 7, Price = 151 } ); } } } 12.4 执行数据库迁移
1)添加连接字符串


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





//需要将master替换为需要创建的数据库名称 Data Source=(local);Initial Catalog=master;Integrated Security=True;Trust Server Certificate=True3)数据库上下文中创建构造函数指定连接数据库位置
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 = "wemen", Size = 11, Price = 132 }, new Shirt { Id = 4, Name = "衬衫4", Color = "白色", Gender = "wemen", Size = 7, Price = 151 } ); } } }4)在Progam.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("Id", $"ID为{id.Value}的衬衫在数据库不存在"); var problemDetails = new ValidationProblemDetails(context.ModelState) { Status = 400, Title = "衬衫ID不存在", }; context.Result = new NotFoundObjectResult(problemDetails); } else { context.HttpContext.Items["shirt"] = 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)创建应用内容存储,AppPepository.cs,替代已存储数据库的应用程序注册信息
namespace WebApiDemo.Authority { public static class AppPepository { 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), expores_at = expiresAt }); } else { ModelState.AddModelError("Unanthorized", "未被授权"); 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 = AppPepository.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 = AppPepository.GetApplicationByClientId(clientId); var clamsDictionary = new Dictionary<string, object> { { "AppName", app?.ApplicationName??string.Empty }, //{ "Read", (app?.Scopes??string.Empty).Contains("read") ? "true" : "false" }, //{ "Write", (app?.Scopes??string.Empty).Contains("write") ? "true" : "false" } }; var scopes = app?.Scopes?.Split(',') ?? Array.Empty<string>(); if(scopes.Length > 0) { foreach(var scope in scopes) { clamsDictionary.Add(scope.Trim().ToLower(), "true"); } } var tokenDescriptor = new SecurityTokenDescriptor { SigningCredentials = signingCredentials, Claims = clamsDictionary, Expires = expiresAt, NotBefore = DateTime.UtcNow, }; var tokenHandler = new JsonWebTokenHandler(); return tokenHandler.CreateToken(tokenDescriptor); } public static async Task<IEnumerable<Claim>?> VerifyTokenAsync(string tokenString, string securityKey) { if(string.IsNullOrWhiteSpace(tokenString) || string.IsNullOrWhiteSpace(securityKey)) { return null; } var keyBytes = System.Text.Encoding.UTF8.GetBytes(securityKey); var tokenHandle = new JsonWebTokenHandler(); var validationParameters = new TokenValidationParameters { ValidateIssuer = false, IssuerSigningKey = new SymmetricSecurityKey(keyBytes), ValidateIssuerSigningKey = false, ValidateAudience = false, ValidateLifetime = true, ClockSkew = TimeSpan.Zero }; try { var result = await tokenHandle.ValidateTokenAsync(tokenString, validationParameters); if(result.SecurityToken != null) { var tokenObject = tokenHandle.ReadJsonWebToken(tokenString); return tokenObject.Claims ?? Enumerable.Empty<Claim>(); } else { return null; } } catch (SecurityTokenMalformedException) { return null; } catch (SecurityTokenExpiredException) { return null; } catch (SecurityTokenInvalidSignatureException) { return null; } catch (Exception) { throw; } } } } 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)) //{ // context.Result = new UnauthorizedResult(); //} var claims = await Authenticator.VerifyTokenAsync(tokenString, securityKey); if (claims == null) { context.Result = new UnauthorizedResult(); // 无效令牌 401 } else { //5. 获取声明需求 var requiredClaism = context.ActionDescriptor.EndpointMetadata .OfType<RequiredClaimAttribute>() .ToList(); if(requiredClaism != null && !requiredClaism.All(rc => claims.Any(c => c.Type.Equals(rc.ClaimType, StringComparison.OrdinalIgnoreCase) && c.Value.Equals(rc.ClaimValue, StringComparison.OrdinalIgnoreCase)))) { context.Result = new StatusCodeResult(403); // 权限不足 403 } } } } } 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; } } }