ASP.NET Core应用程序2:创建RESTful Web服务
1 准备工作
2 理解RESTful Web服务
Web服务最常见的方法是采用具象状态传输(Representational State Transfer,REST)模式。
REST指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或者设计就是RESTful,核心就是面向资源,REST专门针对网络应用设计和开发方式,以降低开发的复杂性,提高系统的可伸缩性。
REST的核心前提是Web服务通过URL和HTTP方法的组合定义API。常用的HTTP方法有Get,Post、Put、Patch、Delete。Put用于更新现有对象,Patch用于更新现有对象的一部分。
3 使用自定义端点创建Web服务
添加文件WebServiceEndpoint类。
public static class WebServiceEndpoint { private static string BASEURL = "api/products"; public static void MapWebService(this IEndpointRouteBuilder app) { app.MapGet($"{BASEURL}/{{ProductId}}", async context => { long key = long.Parse(context.Request.RouteValues["ProductId"] as string); DataContext data = context.RequestServices.GetService<DataContext>(); Product p = data.Products.Find(key); if (p == null) { context.Response.StatusCode = StatusCodes.Status404NotFound; } else { context.Response.ContentType = "application/json"; var json = JsonSerializer.Serialize(p); await context.Response.WriteAsync(json); } }); app.MapGet(BASEURL, async context => { DataContext data = context.RequestServices.GetService<DataContext>(); context.Response.ContentType = "application/json"; await context.Response.WriteAsync( JsonSerializer.Serialize<IEnumerable<Product>>(data.Products)); }); app.MapPost(BASEURL, async context => { DataContext data = context.RequestServices.GetService<DataContext>(); Product p = await JsonSerializer.DeserializeAsync<Product>(context.Request.Body); await data.AddAsync(p); await data.SaveChangesAsync(); context.Response.StatusCode = StatusCodes.Status200OK; }); } }
添加配置路由。
endpoints.MapWebService();
WebServiceEndpoint扩展方法创建了三条路由。
第一个路由接收一个值查询单个Product对象。在浏览器输入http://localhost:5000/api/products/1
。
第二个路由查询所有Product对象,在浏览器输入http://localhost:5000/api/products
。
第三个路由处理Post请求,添加新对象到数据库。不能使用浏览器发送请求,需要使用命令行,
Invoke-RestMethod http://localhost:5000/api/products -Method POST -Body(@{Name="Swimming Goggles";Price=12.75;CategoryId=1;SupplierId=1}|ConvertTo-Json) -ContentType "application/json"
。执行完成后可以使用第二个路由查询一下。
4 使用控制器创建Web服务
端点创建服务的方式有些繁琐,也很笨拙,所以我们使用控制器来创建。
4.1 启用MVC框架
public void ConfigureServices(IServiceCollection services) { ...... services.AddControllers();//定义了MVC框架需要的服务 }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext context) { ...... app.UseEndpoints(endpoints => { endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); //endpoints.MapWebService(); endpoints.MapControllers();//定义了允许控制器处理请求的路由 }); ...... }
4.1 创建控制器
名称以Controller结尾的公共类都是控制器。
添加Controllers文件,并添加ProductsController类。
[Route("api/[controller]")] public class ProductsController : ControllerBase { [HttpGet] public IEnumerable<Product> GetProducts() { var productArr = new Product[] { new Product(){ Name = "Product #1"}, new Product(){ Name = "Product #2"} }; return productArr; } [HttpGet("{id}")] public Product GetProduct() { var product = new Product() { ProductId = 1,Name= "测试产品" }; return product; } }
(1) 理解基类
控制器是从ControllerBase派生,该类提供对MVC框架和底层ASP.NET Core平台特性的访问。
ControllerBase的属性:
- HttpContext:返回当前请求的HttpContext对象;
- ModelState:返回数据验证过程的详细信息。
- Request:返回返回当前请求的HttpRequest对象;
- Response:返回返回当前请求的HttpResponse对象;
- RouteData:返回路由中间件从请求URL中提取的数据;
- User:返回一个对象,描述于当前请求关联的用户;
每次使用控制器类的一个方法处理请求是,都会创建一个控制器类新实例,这意味着上述属性只描述当前请求。
(2) 理解控制器特性
操作方法支持HTTP方法,URL由应用到控制器的特性组合决定。
控制器的URL由Route特性指定,它应用于类。
[Route("api/[controller]")] public class ProductsController : ControllerBase
参数[controller]部分指从控制器类名派生URL,上述的控制器将URL设置为/api/products。
每个操作方法都用一个属性修饰,指定了HTTP方法。
[HttpGet] public IEnumerable<Product> GetProducts()
访问此方法的URL为/api/products。
应用于指定HTTP方法属性也可以用于控制器URL。
[HttpGet("{id}")] public Product GetProduct()
访问此方法的URL为/api/products/{id}。
在编写控制器务必确保URL只映射到一个操作方法。
(3) 理解控制器方法的返回值
控制器提供的好处之一就是MVC框架负责设置响应头,并序列化发送到客户端的数据对象。
使用端点时,必须直接使用JSON序列化一个可写入响应的字符串,并设置Content-Type头来告诉客户端。而控制器方法只需要返回一个对象,其他都是自动处理的。
(4) 在控制器中使用依赖注入
应用程序的服务通过构造器声明处理,同时仍然允许单个方法声明自己的依赖项。
[Route("api/[controller]")] public class ProductsController : ControllerBase { private DataContext _context; public ProductsController(DataContext dataContext) { _context = dataContext; } [HttpGet] public IEnumerable<Product> GetProducts() { return _context.Products; } [HttpGet("{id}")] public Product GetProduct([FromServices] ILogger<ProductsController> logger) { logger.LogDebug("执行GetProduct"); return _context.Products.FirstOrDefault(); } }
(5) 使用模型绑定访问路由数据
MVC框架使用请求URL来查找操作方法参数的值,这个过程称为模型绑定。以下通过请求URL访问操作方法http://localhost:5000/api/products/5。
[HttpGet("{id}")] public Product GetProduct([FromServices] ILogger<ProductsController> logger, long id) { logger.LogDebug("执行GetProduct"); return _context.Products.Find(id); }
(6) 在请求主体中进行模型绑定
用于请求体中允许客户端发送容易由操作方法接收的数据。
[HttpPost] public void SaveProduct([FromBody] Product product) { _context.Products.Add(product); _context.SaveChanges(); }
FromBody属性用于操作参数,它指定应该通过解析请求主体获得该参数值,调用此操作方法是,MVC框架会创建一个新的Product对象,并用参数值填充其属性。
(7) 添加额外的操作
[HttpPut] public void UpdateProduct([FromBody] Product product) { _context.Products.Update(product); _context.SaveChanges(); } [HttpDelete("{id}")] public void DeleteProduct(long id) { _context.Products.Remove(new Product() { ProductId = id }); _context.SaveChanges(); }
5 改进Web服务
5.1 使用异步操作
异步操作允许ASP.NET Core线程处理其他可能被阻塞的请求,增加了应用程序可以同时处理HTTP请求的数量。
[HttpGet] public IAsyncEnumerable<Product> GetProducts() { return _context.Products; } [HttpGet("{id}")] public async Task<Product> GetProduct( [FromServices] ILogger<ProductsController> logger, long id) { logger.LogDebug("执行GetProduct"); return await _context.Products.FindAsync(id); } [HttpPost] public async Task SaveProduct([FromBody] Product product) { await _context.Products.AddAsync(product); await _context.SaveChangesAsync(); } [HttpPut] public async Task UpdateProduct([FromBody] Product product) { _context.Products.Update(product); await _context.SaveChangesAsync(); } [HttpDelete("{id}")] public async Task DeleteProduct(long id) { _context.Products.Remove(new Product() { ProductId = id }); await _context.SaveChangesAsync(); }
5.2 防止过度绑定
为了防止过度绑定出现的异常,安全的方法是创建单独的数据模型类。
namespace MyWebApp.Models { public class ProductBindingTarget { public string Name { get; set; } public decimal Price { get; set; } public long CategoryId { get; set; } public long SupplierId { get; set; } public Product ToProduct() => new Product() { Name = this.Name, Price = this.Price, CategoryId = this.CategoryId, SupplierId = this.SupplierId }; } }
ProductBindingTarget类确保客户端只传递需要的值,而不至于传ProductId属性报错,修改SaveProduct方法参数如下。
[HttpPost] public async Task SaveProduct([FromBody] ProductBindingTarget target) { await _context.Products.AddAsync(target.ToProduct()); await _context.SaveChangesAsync(); }
5.3 使用操作结果
操作方法可以返回一个IActionResult接口的对象,而不必直接使用HttpResponse对象生成它。
ContorllerBase类提供了一组用于创建操作结果对象的方法:
- OK:返回生成200状态码,并在响应体中发送一个可选的数据对象;
- NoContent:返回204状态码;
- BadRequest:返回400状态吗,该方法接收一个可选的模型状态对象向客户端描述问题;
- File:返回200状态吗,为特定类型设置Content-Type头,并将指定文件发送给客户端;
- NotFound:返回404状态码;
- StatusCode:返回会生成一个带有特定状态码的响应;
- Redirect和RedirectPermanent:返回将客户端重定向到指定URL;
- RedirectToRoute和RedirectToRoutePermanent:返回将客户端重定向到使用路由系统创建指定URL;
- LocalRedirect和LocalRedirectPermanent:返回将客户端重定向到应用程序本地的指定URL;
- RedirectToAction和RedirectToActionPermanent:返回将客户端重定向到一个操作方法。
- RedirectToPage和RedirectToPagePermanent:返回将客户端重定向到Razor Pages。
修改GetProduct和SaveProduct方法。
[HttpGet("{id}")] public async Task<IActionResult> GetProduct( long id) { Product p = await _context.Products.FindAsync(id); if(p == null) { return NotFound(); } return Ok(p); } [HttpPost] public async Task<IActionResult> SaveProduct( [FromBody] ProductBindingTarget target) { Product p = target.ToProduct(); await _context.Products.AddAsync(p); await _context.SaveChangesAsync(); return Ok(p); }
(1)执行重定向
[HttpGet("redirect")] public IActionResult Redirect() { return Redirect("/api/products/1"); }
(2)重定向到操作方法
[HttpGet("redirect")] public IActionResult Redirect() { return RedirectToAction(nameof(GetProduct), new { id = 1 }); }
(3)路由重定向
[HttpGet("redirect")] public IActionResult Redirect() { return RedirectToRoute( new { controller = "Products", action = "GetProduct", Id = 1 }); }
5.4 验证数据
[Required] public string Name { get; set; } [Range(1, 1000)] public decimal Price { get; set; } [Range(1, long.MaxValue)] public long CategoryId { get; set; } [Range(1, long.MaxValue)] public long SupplierId { get; set; }
修改SaveProduct方法,创建对象前作属性验证。ModelState是从ControllerBase类继承来的,如果模型绑定过程生成的数据满足验证标准,那么IsValid返回true。如果验证失败,ModelState对象中会向客户描述验证错误。
[HttpPost] public async Task<IActionResult> SaveProduct( [FromBody] ProductBindingTarget target) { if (ModelState.IsValid) { Product p = target.ToProduct(); await _context.Products.AddAsync(p); await _context.SaveChangesAsync(); return Ok(p); } return BadRequest(ModelState); }
5.5 应用API控制器属性
ApiController属性可应用于Web服务控制器类,以更改模型绑定和验证特性的行为。
[ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase
使用ApiController后就不需要使用FromBody从请求中检查ModelState.IsValid属性,自动应用这种普遍的判断,把控制器方法中的代码焦点放在处理应用逻辑。
[HttpPost] public async Task<IActionResult> SaveProduct(ProductBindingTarget target) { Product p = target.ToProduct(); await _context.Products.AddAsync(p); await _context.SaveChangesAsync(); return Ok(p); } [HttpPut] public async Task UpdateProduct(Product product) { _context.Products.Update(product); await _context.SaveChangesAsync(); }
5.6 忽略Null属性
(1)投射选定的属性
[HttpGet("{id}")] public async Task<IActionResult> GetProduct(long id) { Product p = await _context.Products.FindAsync(id); if (p == null) { return NotFound(); } return Ok(new { ProductId = p.ProductId,Name = p.Name, Price = p.Price,CategoryId = p.CategoryId, SupplierId = p.SupplierId }); }
这样做是为了返回有用属性值,以便省略导航属性。
(2)配置JSON序列化器
可将JSON序列化器配置为在序列化对象时省略值为null的属性。
在Stratup类中配置。此配置会影响所有JSON响应
public void ConfigureServices(IServiceCollection services) { ...... services.AddControllers(); services.Configure<JsonOptions>(opts => { opts.JsonSerializerOptions.IgnoreNullValues = true; }); }
本文来自博客园,作者:一纸年华,转载请注明原文链接:https://www.cnblogs.com/nullcodeworld/p/18152300
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步