使用 ASP.NET Web API 建立 OData v4 端點

開放式資料通訊協定 (OData) 是 Web 的資料存取通訊協定。 OData 提供透過 CRUD 作業查詢及運算元據集的統一方式, (建立、讀取、更新和刪除) 。

ASP.NET Web API支援通訊協定的 v3 和 v4。 您甚至可以有與 v3 端點並存執行的 v4 端點。

本教學課程說明如何建立支援 CRUD 作業的 OData v4 端點。

教學課程中使用的軟體版本

  • Web API 5.2
  • OData v4
  • Visual Studio 2017 (在這裡 下載 Visual Studio 2017)
  • Entity Framework 6
  • .NET 4.7.2

教學課程版本

如需 OData 第 3 版,請參閱 建立 OData v3 端點

建立 Visual Studio 專案

在 Visual Studio 的 [檔案] 功能表中,選取 [新增>專案]。

展開[已安裝>的 Visual C#>Web],然後選取[ASP.NET Web 應用程式] (.NET Framework) 範本。 將專案命名為 「ProductService」。

Visual Studio 新專案視窗的螢幕擷取畫面,其中顯示使用 dot NET Framework 建立 A S P 點 NET Web 應用程式的功能表選項。

選取 [確定]。

S P 點 NET Web 應用程式的螢幕擷取畫面,其中顯示可用範本,以 Web A P I 資料夾和核心參考建立應用程式。

選取 [空白] 範本。 在 [新增資料夾和核心參考] 底下,選取 [Web API]。 選取 [確定]。

安裝 OData 套件

從 [工具] 功能表中,選取 [NuGet 封裝管理員]>[封裝管理員主控台]。 在 [套件管理員主控台] 視窗中,輸入:

Install-Package Microsoft.AspNet.Odata

此命令會安裝最新的 OData NuGet 套件。

新增模型類別

模型是物件,代表應用程式中的資料實體。

在 [方案總管] 中,於 Models 資料夾上按一下滑鼠右鍵。 從操作功能表中,選取 [新增>類別]。

方案總管視窗的螢幕擷取畫面,其中醒目提示將模型類別物件新增至專案的路徑。

注意

根據慣例,模型類別會放在 Models 資料夾中,但您不需要在自己的專案中遵循此慣例。

將類別命名為 Product。 在 Product.cs 檔案中,將重複使用的程式碼取代為下列專案:

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
    }
}

屬性 Id 是實體索引鍵。 用戶端可以依索引鍵查詢實體。 例如,若要取得識別碼為 5 的產品,URI 為 /Products(5) 。 屬性 Id 也會是後端資料庫中的主鍵。

啟用 Entity Framework

在本教學課程中,我們將使用 Entity Framework (EF) Code First 來建立後端資料庫。

注意

Web API OData 不需要 EF。 使用任何可將資料庫實體轉譯為模型的資料存取層。

首先,安裝適用于 EF 的 NuGet 套件。 從 [工具] 功能表中,選取 [NuGet 封裝管理員]>[封裝管理員主控台]。 在 [套件管理員主控台] 視窗中,輸入:

Install-Package EntityFramework

開啟 Web.config 檔案,並在configSections元素之後,于組態專案內新增下列區段。

<configuration>
  <configSections>
    <!-- ... -->
  </configSections>

  <!-- Add this: -->
  <connectionStrings>
    <add name="ProductsContext" connectionString="Data Source=(localdb)\mssqllocaldb; 
        Initial Catalog=ProductsContext; Integrated Security=True; MultipleActiveResultSets=True; 
        AttachDbFilename=|DataDirectory|ProductsContext.mdf"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

此設定會新增 LocalDB 資料庫的連接字串。 當您在本機執行應用程式時,將會使用此資料庫。

接下來,將名為 的 ProductsContext 類別新增至 Models 資料夾:

using System.Data.Entity;
namespace ProductService.Models
{
    public class ProductsContext : DbContext
    {
        public ProductsContext() 
                : base("name=ProductsContext")
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

在建構函式中, "name=ProductsContext" 提供連接字串的名稱。

設定 OData 端點

開啟檔案App_Start/WebApiConfig.cs。 新增下列 using 語句:

using ProductService.Models;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;

然後將下列程式碼新增至 Register 方法:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // New code:
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

此程式碼會執行兩件事:

  • (EDM) 建立實體資料模型。
  • 新增路由。

EDM 是資料的抽象模型。 EDM 可用來建立服務元資料檔案。 ODataConventionModelBuilder類別會使用預設命名慣例來建立 EDM。 此方法需要最少的程式碼。 如果您想要進一步控制 EDM,您可以使用 ODataModelBuilder 類別明確地新增屬性、索引鍵和導覽屬性來建立 EDM。

路由會告訴 Web API 如何將 HTTP 要求路由傳送至端點。 若要建立 OData v4 路由,請呼叫 MapODataServiceRoute 擴充方法。

如果您的應用程式有多個 OData 端點,請為每個端點建立個別的路由。 為每個路由提供唯一的路由名稱和前置詞。

新增 OData 控制器

控制器是處理 HTTP 要求的類別。 您會為 OData 服務中的每個實體集建立個別的控制器。 在本教學課程中,您將為 Product 實體建立一個控制器。

在方案總管中,以滑鼠右鍵按一下 Controllers 資料夾,然後選取 [新增>類別]。 將類別命名為 ProductsController

注意

OData v3 的本教學課程版本會使用 新增控制器 Scaffolding。 目前,OData v4 沒有 Scaffolding。

以下列內容取代 ProductsController.cs 中的重複使用程式碼。

using ProductService.Models;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;
namespace ProductService.Controllers
{
    public class ProductsController : ODataController
    {
        ProductsContext db = new ProductsContext();
        private bool ProductExists(int key)
        {
            return db.Products.Any(p => p.Id == key);
        } 
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

控制器會 ProductsContext 使用 類別來使用 EF 存取資料庫。 請注意,控制器會覆寫 Dispose 方法來處置 ProductsCoNtext

這是控制器的起點。 接下來,我們將為所有 CRUD 作業新增方法。

查詢實體集

將下列方法新增至 ProductsController

[EnableQuery]
public IQueryable<Product> Get()
{
    return db.Products;
}
[EnableQuery]
public SingleResult<Product> Get([FromODataUri] int key)
{
    IQueryable<Product> result = db.Products.Where(p => p.Id == key);
    return SingleResult.Create(result);
}

方法的 Get 無參數版本會傳回整個 Products 集合。 具有 Get 索引鍵參數的方法會依其 索引鍵 (查詢產品,在此案例中為 Id 屬性) 。

[EnableQuery]屬性可讓用戶端使用查詢選項來修改查詢,例如$filter、$sort和$page。 如需詳細資訊,請參閱 支援 OData 查詢選項

將實體新增至實體集

若要讓用戶端將新產品新增至資料庫,請將下列方法新增至 ProductsController

public async Task<IHttpActionResult> Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Created(product);
}

更新實體

OData 支援兩個不同的語意來更新實體 PATCH 和 PUT。

  • PATCH 會執行部分更新。 用戶端只會指定要更新的屬性。
  • PUT 會取代整個實體。

PUT 的缺點是用戶端必須傳送實體中所有屬性的值,包括未變更的值。 OData 規格指出 PATCH 是慣用的。

在任何情況下,以下是 PATCH 和 PUT 方法的程式碼:

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var entity = await db.Products.FindAsync(key);
    if (entity == null)
    {
        return NotFound();
    }
    product.Patch(entity);
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(entity);
}
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    if (key != update.Id)
    {
        return BadRequest();
    }
    db.Entry(update).State = EntityState.Modified;
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(update);
}

在 PATCH 的情況下,控制器會使用Delta < T >類型來追蹤變更。

刪除實體

若要讓用戶端從資料庫刪除產品,請將下列方法新增至 ProductsController

public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
    var product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return StatusCode(HttpStatusCode.NoContent);
}