第19章 建立RESTful Web服務

一纸年华發表於2024-04-23

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

相關文章