Web API 2 を使用した OData v3 でのエンティティ関係のサポート

作成者: Mike Wasson

完成したプロジェクトのダウンロード

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

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

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

  • Web API 2
  • OData Version 3
  • Entity Framework 6

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

まず、OData フィードに新しいエンティティ型を追加する必要があります。 Supplier クラスを追加します。

using System.ComponentModel.DataAnnotations;

namespace ProductService.Models
{
    public class Supplier
    {
        [Key]
        public string Key { get; set; }
        public string Name { get; set; }
    }
}

このクラスでは、エンティティ キーに文字列を使用します。 実際には、整数キーを使用するよりも一般的ではない可能性があります。 ただし、OData が整数以外のキー型を処理する方法を確認する価値があります。

次に、 Product クラスに Supplier プロパティを追加してリレーションを作成します。

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 string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

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

public class ProductServiceContext : DbContext
{
    public ProductServiceContext() : base("name=ProductServiceContext")
    {
    }

    public System.Data.Entity.DbSet<ProductService.Models.Product> Products { get; set; }
    // New code:
    public System.Data.Entity.DbSet<ProductService.Models.Supplier> Suppliers { get; set; }
}

WebApiConfig.csで、EDM モデルに "Suppliers" エンティティを追加します。

ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
// New code:
builder.EntitySet<Supplier>("Suppliers");

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

GET /Products(1)/Supplier

ここで、"Supplier" は型の Product ナビゲーション プロパティです。 この場合、Supplier は 1 つの項目を参照しますが、ナビゲーション プロパティはコレクション (一対多または多対多の関係) を返すこともできます。

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

// GET /Products(1)/Supplier
public Supplier GetSupplier([FromODataUri] int key)
{
    Product product = _context.Products.FirstOrDefault(p => p.ID == key);
    if (product == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return product.Supplier;
}

key パラメーターは、製品のキーです。 メソッドは、関連エンティティ (この場合は Supplier インスタンス) を返します。 メソッド名とパラメーター名はどちらも重要です。 一般に、ナビゲーション プロパティの名前が "X" の場合は、"GetX" という名前のメソッドを追加する必要があります。 メソッドは、親のキーのデータ型と一致する "key" という名前のパラメーターを受け取る必要があります。

key パラメーターに [FromOdataUri] 属性を含めることも重要です。 この属性は、要求 URI からキーを解析するときに OData 構文規則を使用するように Web API に指示します。

OData では、2 つのエンティティ間のリレーションシップの作成または削除がサポートされています。 OData の用語では、リレーションシップは "リンク" です。各リンクには、 フォーム エンティティ/$links/エンティティを含む URI があります。 たとえば、製品からサプライヤーへのリンクは次のようになります。

/Products(1)/$links/Supplier

新しいリンクを作成するために、クライアントはリンク URI に POST 要求を送信します。 要求の本文は、ターゲット エンティティの URI です。 たとえば、キー "CTSO" を持つサプライヤーがあるとします。 "Product(1)" から "Supplier('CTSO')" へのリンクを作成するために、クライアントは次のような要求を送信します。

POST http://localhost/odata/Products(1)/$links/Supplier
Content-Type: application/json
Content-Length: 50

{"url":"http://localhost/odata/Suppliers('CTSO')"}

リンクを削除するために、クライアントは DELETE 要求をリンク URI に送信します。

リンクの作成

クライアントが製品サプライヤーのリンクを作成できるようにするには、ProductsController クラスに次のコードを追加します。

[AcceptVerbs("POST", "PUT")]
public async Task<IHttpActionResult> CreateLink([FromODataUri] int key, string navigationProperty, [FromBody] Uri link)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
            
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
            
    switch (navigationProperty)
    {
        case "Supplier":
            string supplierKey = GetKeyFromLinkUri<string>(link);
            Supplier supplier = await db.Suppliers.FindAsync(supplierKey);
            if (supplier == null)
            {
                return NotFound();
            }
            product.Supplier = supplier;
            await db.SaveChangesAsync();
            return StatusCode(HttpStatusCode.NoContent);

        default:
            return NotFound();
    }
}

このメソッドには、次の 3 つのパラメーターがあります。

  • key: 親エンティティ (製品) のキー
  • navigationProperty: ナビゲーション プロパティの名前。 この例では、有効なナビゲーション プロパティは "Supplier" のみです。
  • link: 関連エンティティの OData URI。 この値は要求本文から取得されます。 たとえば、リンク URI は "http://localhost/odata/Suppliers('CTSO')" になる場合があります。これは、ID = 'CTSO' のサプライヤーを意味します。

このメソッドでは、リンクを使用してサプライヤーを検索します。 一致するサプライヤーが見つかった場合、メソッドは Product.Supplier プロパティを設定し、結果をデータベースに保存します。

最も難しい部分は、リンク URI の解析です。 基本的には、その URI に GET 要求を送信した結果をシミュレートする必要があります。 次のヘルパー メソッドは、これを行う方法を示しています。 メソッドは Web API ルーティング プロセスを呼び出し、解析された OData パスを表す ODataPath インスタンスを取得します。 リンク URI の場合、セグメントの 1 つがエンティティ キーである必要があります。 (そうでない場合、クライアントは無効な URI を送信しました。)

// Helper method to extract the key from an OData link URI.
private TKey GetKeyFromLinkUri<TKey>(Uri link)
{
    TKey key = default(TKey);

    // Get the route that was used for this request.
    IHttpRoute route = Request.GetRouteData().Route;

    // Create an equivalent self-hosted route. 
    IHttpRoute newRoute = new HttpRoute(route.RouteTemplate, 
        new HttpRouteValueDictionary(route.Defaults), 
        new HttpRouteValueDictionary(route.Constraints),
        new HttpRouteValueDictionary(route.DataTokens), route.Handler);

    // Create a fake GET request for the link URI.
    var tmpRequest = new HttpRequestMessage(HttpMethod.Get, link);

    // Send this request through the routing process.
    var routeData = newRoute.GetRouteData(
        Request.GetConfiguration().VirtualPathRoot, tmpRequest);

    // If the GET request matches the route, use the path segments to find the key.
    if (routeData != null)
    {
        ODataPath path = tmpRequest.GetODataPath();
        var segment = path.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
        if (segment != null)
        {
            // Convert the segment into the key type.
            key = (TKey)ODataUriUtils.ConvertFromUriLiteral(
                segment.Value, ODataVersion.V3);
        }
    }
    return key;
}

リンクの削除

リンクを削除するには、次のコードを ProductsController クラスに追加します。

public async Task<IHttpActionResult> DeleteLink([FromODataUri] int key, string navigationProperty)
{
    Product product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }

    switch (navigationProperty)
    {
        case "Supplier":
            product.Supplier = null;
            await db.SaveChangesAsync();
            return StatusCode(HttpStatusCode.NoContent);

        default:
            return NotFound();

    }
}

この例では、ナビゲーション プロパティは 1 つの Supplier エンティティです。 ナビゲーション プロパティがコレクションの場合、リンクを削除する URI には、関連エンティティのキーを含める必要があります。 次に例を示します。

DELETE /odata/Customers(1)/$links/Orders(1)

この要求により、顧客 1 から注文 1 が削除されます。 この場合、DeleteLink メソッドには次のシグネチャがあります。

void DeleteLink([FromODataUri] int key, string relatedKey, string navigationProperty);

relatedKey パラメーターは、関連エンティティのキーを指定します。 そのため、メソッド DeleteLinkkey パラメーターでプライマリ エンティティを検索し、relatedKey パラメーターで関連エンティティを見つけて、関連付けを削除します。 データ モデルによっては、DeleteLink の両方のバージョンを実装する必要がある場合があります。 Web API は、要求 URI に基づいて正しいバージョンを呼び出します。