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