ASP.NET Web API を使用して OData v4 エンドポイントを作成する

Open Data Protocol (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」という名前を付けます。

Screenshot of the visual studio new project window, showing menu options to create an A S P dot NET Web Application with the dot NET Framework.

[OK] を選択します。

Screenshot of the A S P dot NET Web Application, showing available templates to create the application with a Web A P I folder and core reference.

[空] テンプレートを選択します。 [フォルダーとコア参照の追加対象:] で、[Web API] を選択します。 [OK] を選択します。

OData パッケージをインストールする

[ツール] メニューで、[NuGet パッケージ マネージャー]>[パッケージ マネージャー コンソール] の順に選択します。 [パッケージ マネージャー コンソール] ウィンドウに、次のように入力します。

Install-Package Microsoft.AspNet.Odata

このコマンドにより、最新の OData NuGet パッケージがインストールされます。

モデル クラスの追加

"モデル" は、アプリケーションのデータ エンティティを表すオブジェクトです。

ソリューション エクスプローラーで、[モデル] フォルダーを右クリックします。 コンテキスト メニューで、[追加]>[クラス] を選択します。

Screenshot of the solution explorer window, highlighting the path to add a model class object to the project.

Note

慣例により、モデル クラスは 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 プロパティはエンティティ キーです。 クライアントは、キーを使用してエンティティを照会できます。 たとえば、ID が 5 の製品を取得する URI は /Products(5) になります。 Id プロパティは、バックエンド データベースの主キーでもあります。

Entity Framework を有効にする

このチュートリアルでは、Entity Framework (EF) Code First を使用してバックエンド データベースを作成します。

Note

Web API OData には EF は必要ありません。 データベース エンティティをモデルに変換できる任意のデータ アクセス レイヤーを使用します。

まず、EF 用の NuGet パッケージをインストールします。 [ツール] メニューで、[NuGet パッケージ マネージャー]>[パッケージ マネージャー コンソール] の順に選択します。 [パッケージ マネージャー コンソール] ウィンドウに、次のように入力します。

Install-Package EntityFramework

Web.config ファイルを開き、configuration 要素内の 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 データベースの接続文字列が追加されます。 このデータベースは、アプリをローカルで実行するときに使用されます。

次に、Models フォルダーに ProductsContext という名前のクラスを追加します。

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

このコードは 2 つのことを行います:

  • Entity Data Model (EDM) を作成します。
  • ルートを追加します。

EDM は、データの抽象モデルです。 EDM は、サービス メタデータ ドキュメントを作成するために使用されます。 ODataConventionModelBuilder クラスは、既定の名前付け規則を使用して EDM を作成します。 このアプローチでは、最も少ないコードで済みます。 EDM をより詳細に制御する場合は、ODataModelBuilder クラスを使用して、プロパティ、キー、およびナビゲーション プロパティを明示的に追加して EDM を作成できます。

"ルート" は、HTTP 要求をエンドポイントにルーティングする方法を Web API に指示します。 OData v4 のルートを作成するには、MapODataServiceRoute 拡張メソッドを呼び出します。

アプリケーションに複数の OData エンドポイントがある場合は、それぞれに個別のルートを作成します。 各ルートに一意のルート名とプレフィックスを付けます。

OData コントローラーを追加する

コントローラー は、HTTP 要求を処理するクラスです。 OData サービス内のエンティティ セットごとに個別のコントローラーを作成します。 このチュートリアルでは、Product エンティティ用のコントローラーを 1 つ作成します。

ソリューション エクスプローラーで、Controllers フォルダーを右クリックし、[追加]>[クラス] を選択します。 クラスに ProductsController という名前を付けます。

Note

このチュートリアルの OData v3 のバージョンでは、コントローラーの追加スキャフォールディングを使用します。 現時点では、OData v4 のスキャフォールディングはありません。

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 コレクション全体を返します。 key パラメーターが指定された 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) を更新するための 2 つの異なるセマンティクスがサポートされています。

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