ASP.NET Web API 2.2 を使用した OData v4 のエンティティ関係

作成者: Mike Wasson

ほとんどのデータ セットはエンティティ間の関係を定義します。顧客には注文があります。書籍には著者がいます。製品にはサプライヤーがいます。 OData を使用すると、クライアントはエンティティ関係をナビゲートできます。 製品を指定すると、サプライヤーを見つけることができます。 リレーションシップを作成または削除することもできます。 たとえば、製品の仕入先を設定できます。

このチュートリアルでは、ASP.NET Web API を使用して OData v4 でこれらの操作をサポートする方法について説明します。 このチュートリアルは、「ASP.NET Web API 2 を使用して OData v4 エンドポイントを作成する」チュートリアルに基づいています。

チュートリアルで使用するソフトウェアのバージョン

  • Web API 2.1
  • OData v4
  • Visual Studio 2017 (ダウンロード Visual Studio 2017 はこちら)
  • Entity Framework 6
  • .NET 4.5

チュートリアルのバージョン

OData バージョン 3 については、OData v3 でのエンティティ関係のサポートに関するページを参照してください。

サプライヤー エンティティを追加する

Note

このチュートリアルは、「ASP.NET Web API 2 を使用して OData v4 エンドポイントを作成する」チュートリアルに基づいています。

まず、関連エンティティが必要です。 Models フォルダーに Supplier という名前のクラスを追加します。

using System.Collections.Generic;

namespace ProductService.Models
{
    public class Supplier
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public ICollection<Product> Products { get; set; }
    }
}

ナビゲーション プロパティを Product クラスに追加します。

using System.ComponentModel.DataAnnotations.Schema;

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

        // New code:    
        [ForeignKey("Supplier")]
        public int? SupplierId { get; set; }
        public virtual Supplier Supplier { get; set; }
    }
}

新しい DbSetProductsContext クラスに追加して、Entity Framework がデータベースに Supplier テーブルを含めるようにします。

public class ProductsContext : DbContext
{
    static ProductsContext()
    {
        Database.SetInitializer(new ProductInitializer());
    }

    public DbSet<Product> Products { get; set; }
    // New code:
    public DbSet<Supplier> Suppliers { get; set; }
}

WebApiConfig.csで、エンティティ データ モデルに "Suppliers" エンティティ セットを追加します。

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

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

Controllers フォルダーに SuppliersController クラスを追加します。

using ProductService.Models;
using System.Linq;
using System.Web.OData;

namespace ProductService.Controllers
{
    public class SuppliersController : ODataController
    {
        ProductsContext db = new ProductsContext();

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

このコントローラーに CRUD 操作を追加する方法は示しません。 手順は Products コントローラーの場合と同じです (OData v4 エンドポイントの作成に関するページを参照してください)。

製品のサプライヤーを取得するために、クライアントは GET 要求を送信します。

GET /Products(1)/Supplier

この要求をサポートするには、ProductsController クラスに次のメソッドを 追加します。

public class ProductsController : ODataController
{
    // GET /Products(1)/Supplier
    [EnableQuery]
    public SingleResult<Supplier> GetSupplier([FromODataUri] int key)
    {
        var result = db.Products.Where(m => m.Id == key).Select(m => m.Supplier);
        return SingleResult.Create(result);
    }
 
   // Other controller methods not shown.
}

このメソッドは、既定の名前付け規則を使用します

  • メソッド名: GetX。X はナビゲーション プロパティです。
  • パラメーター名: key

この名前付け規則に従うと、Web API によって HTTP 要求がコントローラー メソッドに自動的にマップされます。

HTTP 要求の例:

GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 応答の例:

HTTP/1.1 200 OK
Content-Length: 125
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 00:44:27 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Suppliers/$entity","Id":2,"Name":"Wingtip Toys"
}

前の例では、1 つの製品に 1 つのサプライヤーがあります。 ナビゲーション プロパティは、コレクションを返すこともできます。 次のコードは、サプライヤーの製品を取得します。

public class SuppliersController : ODataController
{
    // GET /Suppliers(1)/Products
    [EnableQuery]
    public IQueryable<Product> GetProducts([FromODataUri] int key)
    {
        return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products);
    }

    // Other controller methods not shown.
}

この場合、メソッドは SingleResult<T> の代わりに IQueryable を返します

HTTP 要求の例:

GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 応答の例:

HTTP/1.1 200 OK
Content-Length: 372
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 01:06:54 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Products","value":[
    {
      "Id":1,"Name":"Hat","Price":14.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":2,"Name":"Socks","Price":6.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":4,"Name":"Pogo Stick","Price":29.99,"Category":"Toys","SupplierId":2
    }
  ]
}

エンティティ間のリレーションシップの作成

OData では、2 つの既存のエンティティ間のリレーションシップの作成または削除がサポートされています。 OData v4 の用語では、リレーションシップは "参照" です。 (OData v3 では、リレーションシップは "リンク" と呼ばれていました。このチュートリアルでは、プロトコルの違いは関係ありません)。

参照には、フォーム /Entity/NavigationProperty/$ref を含む独自の URI があります。 たとえば、ある製品とそのサプライヤー間の参照に対応する URI を次に示します。

http:/host/Products(1)/Supplier/$ref

リレーションシップを追加するために、クライアントはこのアドレスに POST または PUT 要求を送信します。

  • ナビゲーション プロパティが 1 つのエンティティである場合は PUT (例: Product.Supplier)。
  • ナビゲーション プロパティがコレクションの場合は POST (例: Supplier.Products)。

要求の本文には、リレーションシップ内の他のエンティティの URI が含まれています。 要求の例を以下に示します。

PUT http://myproductservice.example.com/Products(6)/Supplier/$ref HTTP/1.1
OData-Version: 4.0;NetFx
OData-MaxVersion: 4.0;NetFx
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Content-Type: application/json;odata.metadata=minimal
User-Agent: Microsoft ADO.NET Data Services
Host: myproductservice.example.com
Content-Length: 70
Expect: 100-continue

{"@odata.id":"http://myproductservice.example.com/Suppliers(4)"}

この例では、クライアントは PUT 要求を /Products(6)/Supplier/$ref に送信します(これは、ID = 6 の製品の Supplier の $ref URI です)。 要求が成功した場合、サーバーは 204 (コンテンツなし) 応答を送信します。

HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT

リレーションシップを Product に追加するコントローラー メソッドを次に示します。

public class ProductsController : ODataController
{
    [AcceptVerbs("POST", "PUT")]
    public async Task<IHttpActionResult> CreateRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = await db.Products.SingleOrDefaultAsync(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }
        switch (navigationProperty)
        {
            case "Supplier":
                // Note: The code for GetKeyFromUri is shown later in this topic.
                var relatedKey = Helpers.GetKeyFromUri<int>(Request, link);
                var supplier = await db.Suppliers.SingleOrDefaultAsync(f => f.Id == relatedKey);
                if (supplier == null)
                {
                    return NotFound();
                }

                product.Supplier = supplier;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();
        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

navigationProperty パラメーターは、設定するリレーションシップを指定します。 (エンティティに複数のナビゲーション プロパティがある場合は、さらに case ステートメントを追加できます)。

link パラメーターには、サプライヤーの URI が含まれています。 Web API は、要求本文を自動的に解析して、このパラメーターの値を取得します。

サプライヤーを検索するには、link パラメーターの一部である ID (またはキー) が必要です。 これを行うには、次のヘルパー メソッドを使用します。

using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;

namespace ProductService
{
    public static class Helpers
    {
        public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

            string serviceRoot = urlHelper.CreateODataLink(
                request.ODataProperties().RouteName, 
                request.ODataProperties().PathHandler, new List<ODataPathSegment>());
            var odataPath = request.ODataProperties().PathHandler.Parse(
                request.ODataProperties().Model, 
                serviceRoot, uri.LocalPath);

            var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
            if (keySegment == null)
            {
                throw new InvalidOperationException("The link does not contain a key.");
            }

            var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
            return (TKey)value;
        }

    }
}

基本的に、このメソッドは OData ライブラリを使用して URI パスをセグメントに分割し、キーを含むセグメントを見つけて、キーを正しい型に変換します。

エンティティ間のリレーションシップの削除

リレーションシップを削除するために、クライアントにより HTTP DELETE 要求が $ref URI に送信されます。

DELETE http://host/Products(1)/Supplier/$ref

製品とサプライヤーのリレーションシップを削除するコントローラー メソッドを次に示します。

public class ProductsController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = db.Products.SingleOrDefault(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }

        switch (navigationProperty)
        {
            case "Supplier":
                product.Supplier = null;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }        

    // Other controller methods not shown.
}

この場合、Product.Supplier は、1 対多のリレーションシップの "1" 側です。そのため、Product.Suppliernull に設定するだけでリレーションシップを削除できます。

リレーションシップの "多" 側では、クライアントは削除する関連エンティティを指定する必要があります。 これを行うために、クライアントは要求のクエリ文字列内の関連エンティティの URI を送信します。 たとえば、"Supplier 1" から "Product 1" を削除するには、次のようにします。

DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)

Web API でこれをサポートするには、DeleteRef メソッドに追加のパラメーターを含める必要があります。 Supplier.Products 関係から製品を削除するコントローラー メソッドを次に示します。

public class SuppliersController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        [FromODataUri] string relatedKey, string navigationProperty)
    {
        var supplier = await db.Suppliers.SingleOrDefaultAsync(p => p.Id == key);
        if (supplier == null)
        {
            return StatusCode(HttpStatusCode.NotFound);
        }

        switch (navigationProperty)
        {
            case "Products":
                var productId = Convert.ToInt32(relatedKey);
                var product = await db.Products.SingleOrDefaultAsync(p => p.Id == productId);

                if (product == null)
                {
                    return NotFound();
                }
                product.Supplier = null;
                break;
            default:
                return StatusCode(HttpStatusCode.NotImplemented);

        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

key パラメーターは仕入先のキーであり、relatedKey パラメーターは Products リレーションシップから削除する製品のキーです。 Web API はクエリ文字列から自動的にキーを取得することに注意してください。