使用 ASP.NET Web API 建立 OData 服務

OData 從 2013 年首次出現後就吸引不少技術人員目光,剛好當時專案允許立馬就嘗試,以當時的專案需要同時提供 API 給 WebSite、iOS 及 Android,同樣的資料內容在不同畫面上會因為裝置大小或是版面問題而需要不同欄位,透過 OData 來處理這樣的需求確實方便不少。

最近則是因為專案時程因素,所以預計先長個 OData API 讓前後端的資料可以先銜接起來,日後如果需要調整再來處理(據經驗,通常這樣的規劃在系統上線後就會變成系統穩定就不用改了XD),無論如何還是來紀錄一下如何建立 OData 服務吧

基本環境

  1. Visual Studio 2017
  2. .NET Framework 4.6.2
  3. ASP.NET Web API 2.2
  4. OData v4
  5. Entity Framework 6.1.3

建立 ASP.NET Web API 專案

  • 建立專案 –> Web –> ASP.NET Web Application

    1webapi

  • ASP.NET Empty template –> Web API

    2empty

選項一:使用 scaffolding 建立 OData 服務

  1. Controllers 資料夾上按右鍵 –> Add –> Controller…

    3efcontroller

  2. Controller –> Web API 2 OData v3 Controller with actions,using Entity Framework

    4template

  3. 指定 Model 、 context 與 Controller name

    5controller

  • 需要注意的是使用 scaffolding 來建立 OData 的做法僅限 OData v3 版本,OData v4 並不支援
  • 過程中會自動安裝以下套件

    • Microsoft.AspNet.WebApi.OData 5.3.1

      2017/10/10 最新版為 5.7.0

    • Microsoft.Data.Edm 5.6.0

      2017/10/10 最新版為 5.8.3

    • Microsoft.Data.OData 5.6.0

      2017/10/10 最新版為 5.8.3

    • System.Spatial 5.6.0

      2017/10/10 最新版為 5.8.3

選項二:手動建立 OData 服務

  • 以下操作需先完成 EntityFramework 連線 DB 設定

    • Models –> Add –> ADO.NET Enityt Data Model

      10adoentity

    • Code First from data base

      11datamodel

    • Choose Data Connection

      12dataconnection

    • Choose Data Objects

      13dataobject

  1. 安裝 Microsoft.AspNet.Odata 套件

    注意這邊的 Microsoft.AspNet.Odata 與上面使用的 Microsoft.AspNet.WebApi.OData 不同

    • Microsoft.AspNet.Odata 是 OData v4 用

      6odatav4

    • Microsoft.AspNet.WebApi.OData 則是 OData v1-v3 用

      7odatav3

    • 使用 Package Manager Console

      Install-Package Microsoft.AspNet.Odata

      8packageocnsole

    • 使用 NuGet Package Manager

      9packagemanager

    • 如果安裝很久出現錯誤或是遲遲無法完成安裝,可以改用 VS2015 安裝看看,我卡了好久改用 VS2015 才成功

  2. 建立 OData controller

    以測試 db - Northwind 為例

    • 建立 ShippersController

      加入 ShippersController.cs

      public class ShippersController : ODataController
      {
      }
      
    • 透過 EntityFramework 檢查資料

      NorthwindEntities db = new NorthwindEntities();
      private bool ShippersExists(int key)
      {
          return db.Shippers.Any(p => p.ShipperID == key);
      }
      
    • 加上 Dispose

      protected override void Dispose(bool disposing)
      {
          db.Dispose();
          base.Dispose(disposing);
      }
      
    • 完整程式碼

      using System.Linq;
      using System.Web.OData;
      using TestOData.Models;
              
      namespace TestOData.Controllers
      {
          public class ShippersController: ODataController
          {
              NorthwindEntities db = new NorthwindEntities();
              private bool ShippersExists(int key)
              {
                  return db.Shippers.Any(p => p.ShipperID == key);
              }
              protected override void Dispose(bool disposing)
              {
                  db.Dispose();
                  base.Dispose(disposing);
              }
          }
      }
      
  3. 註冊 OData 服務

    • 開啟 App_Start/WebApiConfig.cs
    • 註冊 OData routeing

      將新增的程式碼加至 Register 最後

      public static void Register(HttpConfiguration config)
      {
          // New code:
          ODataModelBuilder builder = new ODataConventionModelBuilder();
          builder.EntitySet<Shippers>("Shippers");
          config.MapODataServiceRoute(
              routeName: "ODataRoute",
              routePrefix: "odata",
              model: builder.GetEdmModel());
      }
      
    • 完整程式碼

      using System.Net.Http.Formatting;
      using System.Web.Http;
      using System.Web.OData.Builder;
      using System.Web.OData.Extensions;
      using TestOData.Models;
              
      namespace TestOData
      {
          public static class WebApiConfig
          {
              public static void Register(HttpConfiguration config)
              {
                  // Web API configuration and services
                  config.Formatters.Clear();
                  config.Formatters.Add(new JsonMediaTypeFormatter());
                  // Web API routes
                  config.MapHttpAttributeRoutes();
                              config.Routes.MapHttpRoute(
                      name: "DefaultApi",
                      routeTemplate: "api/{controller}/{id}",
                      defaults: new { id = RouteParameter.Optional }
                  );
                              // New code:
                  ODataModelBuilder builder = new ODataConventionModelBuilder();
                  builder.EntitySet<Shippers>("Shippers");
                  config.MapODataServiceRoute(
                      routeName: "ODataRoute",
                      routePrefix: "odata",
                      model: builder.GetEdmModel());
              }
          }
      }
      
  4. 加入 CRUD

    • Get - Read

      [EnableQuery]
      public IQueryable<Shippers> Get()
      {
          return db.Shippers;
      }
      [EnableQuery]
      public SingleResult<Shippers> Get([FromODataUri] int key)
      {
          IQueryable<Shippers> result = db.Shippers.Where(p => p.ShipperID == key);
          return SingleResult.Create(result);
      }
      
    • Post - Create

      public async Task<IHttpActionResult> Post(Shippers shippers)
      {
          if (!ModelState.IsValid)
          {
              return BadRequest(ModelState);
          }
          db.Shippers.Add(shippers);
          await db.SaveChangesAsync();
          return Created(shippers);
      }
      
    • Put/Patch - Update

      public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Shippers> shippers)
      {
          if (!ModelState.IsValid)
          {
              return BadRequest(ModelState);
          }
          var entity = await db.Shippers.FindAsync(key);
          if (entity == null)
          {
              return NotFound();
          }
          shippers.Patch(entity);
          try
          {
              await db.SaveChangesAsync();
          }
          catch (DbUpdateConcurrencyException)
          {
              if (!ShippersExists(key))
              {
                  return NotFound();
              }
              else
              {
                  throw;
              }
          }
          return Updated(entity);
      }
      
      public async Task<IHttpActionResult> Put([FromODataUri] int key, Shippers update)
      {
          if (!ModelState.IsValid)
          {
              return BadRequest(ModelState);
          }
          if (key != update.ShipperID)
          {
              return BadRequest();
          }
          db.Entry(update).State = EntityState.Modified;
          try
          {
              await db.SaveChangesAsync();
          }
          catch (DbUpdateConcurrencyException)
          {
              if (!ShippersExists(key))
              {
                  return NotFound();
              }
              else
              {
                  throw;
              }
          }
          return Updated(update);
      }
      
    • Delete - Delete

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

      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;
      using TestOData.Models;
              
      namespace TestOData.Controllers
      {
          public class ShippersController: ODataController
          {
              NorthwindEntities db = new NorthwindEntities();
              [EnableQuery]
              public IQueryable<Shippers> Get()
              {
                  return db.Shippers;
              }
              [EnableQuery]
              public SingleResult<Shippers> Get([FromODataUri] int key)
              {
                  IQueryable<Shippers> result = db.Shippers.Where(p => p.ShipperID == key);
                  return SingleResult.Create(result);
              }
              public async Task<IHttpActionResult> Post(Shippers shippers)
              {
                  if (!ModelState.IsValid)
                  {
                      return BadRequest(ModelState);
                  }
                  db.Shippers.Add(shippers);
                  await db.SaveChangesAsync();
                  return Created(shippers);
              }
              public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Shippers> shippers)
              {
                  if (!ModelState.IsValid)
                  {
                      return BadRequest(ModelState);
                  }
                  var entity = await db.Shippers.FindAsync(key);
                  if (entity == null)
                  {
                      return NotFound();
                  }
                  shippers.Patch(entity);
                  try
                  {
                      await db.SaveChangesAsync();
                  }
                  catch (DbUpdateConcurrencyException)
                  {
                      if (!ShippersExists(key))
                      {
                          return NotFound();
                      }
                      else
                      {
                          throw;
                      }
                  }
                  return Updated(entity);
              }
              public async Task<IHttpActionResult> Put([FromODataUri] int key, Shippers update)
              {
                  if (!ModelState.IsValid)
                  {
                      return BadRequest(ModelState);
                  }
                  if (key != update.ShipperID)
                  {
                      return BadRequest();
                  }
                  db.Entry(update).State = EntityState.Modified;
                  try
                  {
                      await db.SaveChangesAsync();
                  }
                  catch (DbUpdateConcurrencyException)
                  {
                      if (!ShippersExists(key))
                      {
                          return NotFound();
                      }
                      else
                      {
                          throw;
                      }
                  }
                  return Updated(update);
              }
              public async Task<IHttpActionResult> Delete([FromODataUri] int key)
              {
                  var product = await db.Shippers.FindAsync(key);
                  if (product == null)
                  {
                      return NotFound();
                  }
                  db.Shippers.Remove(product);
                  await db.SaveChangesAsync();
                  return StatusCode(HttpStatusCode.NoContent);
              }
              private bool ShippersExists(int key)
              {
                  return db.Shippers.Any(p => p.ShipperID == key);
              }
              protected override void Dispose(bool disposing)
              {
                  db.Dispose();
                  base.Dispose(disposing);
              }
          }
      }
      

實際效果

  • Get

    http://localhost:6600/odata/Shippers

    14get

  • Post

    http://localhost:6600/odata/Shippers

    15post

  • Patch

    http://localhost:6600/odata/Shippers(5)

    16patch

  • Put

    http://localhost:6600/odata/Shippers(5)

    17put

  • Delete

    http://localhost:6600/odata/Shippers(5)

    18delete

心得

這篇筆記花了好幾天才寫完,陸陸續續遇到不少問題,也卡關了好幾次,但終究是搞定了~~ 呼

雖然不知道原因,但可以確定的是 OData v4 有一些跟之前版本不同的 breaking change 讓使用過之前版本的工程師佔不到便宜,還好趁著這次紀錄建立過程釐清不少相關設定,降低系統上線時因為不熟悉設定造成問題的機率

僅管對於設定有比較了解一些,但對於相關原理及進階使用方式的掌握度還是有待加強,相信隨著專案的進行會一步一步更加清晰,到時有什麼值得紀錄介紹的再跟大家分享

參考資訊

  1. OData in ASP.NET Web API
  2. OData Error: The query specified in the URI is not valid. The property cannot be used in the query option
  3. GETTING STARTED WITH WEB API AND ODATA V4 PART 1