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;
});
}