パート 6: Product コントローラーと Order コントローラーの作成

作成者: Rick Anderson

完成したプロジェクトをダウンロードする

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

Admin コントローラーは、管理者権限を持つユーザーを対象としています。 一方、顧客は製品を表示できますが、製品を作成、更新、または削除することはできません。

Get メソッドを開いたまま、Post、Put、Delete メソッドへのアクセスを簡単に制限できます。 ただし、製品に対して返されるデータに注目してください。

{"Id":1,"Name":"Tomato Soup","Price":1.39,"ActualCost":0.99}

ActualCost プロパティは顧客に表示されないようにしてください。 ソリューションは、顧客に表示する必要があるプロパティのサブセットを含むデータ転送オブジェクト (DTO) を定義することです。 LINQ を使用して、Product インスタンスを ProductDTO インスタンスに投影します。

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

namespace ProductStore.Models
{
    public class ProductDTO
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

次に、コントローラーを追加します。 ソリューション エクスプローラーで、[コントローラー] フォルダーを右クリックします。 [追加] を選択し、[コントローラー] を選択します。 [コントローラーの追加] ダイアログで、コントローラーに "ProductsController" という名前を付けます。 [テンプレート] で、[空の API コントローラー] を選択します。

Screenshot of the add controller dialogue box.

ソース ファイル内のすべてを、次のコードに置き換えます。

namespace ProductStore.Controllers
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;
    using ProductStore.Models;

    public class ProductsController : ApiController
    {
        private OrdersContext db = new OrdersContext();

        // Project products to product DTOs.
        private IQueryable<ProductDTO> MapProducts()
        {
            return from p in db.Products select new ProductDTO() 
                { Id = p.Id, Name = p.Name, Price = p.Price };
        }

        public IEnumerable<ProductDTO> GetProducts()
        {
            return MapProducts().AsEnumerable();
        }

        public ProductDTO GetProduct(int id)
        {
            var product = (from p in MapProducts() 
                           where p.Id == 1 
                           select p).FirstOrDefault();
            if (product == null)
            {
                throw new HttpResponseException(
                    Request.CreateResponse(HttpStatusCode.NotFound));
            }
            return product;
        }

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

コントローラーは、引き続きデータベースにクエリを実行するための OrdersContext に使用します。 しかし、Product インスタンスを直接返す代わりに、ProductDTO インスタンスに投影するために MapProducts を呼び出します。

return from p in db.Products select new ProductDTO() 
    { Id = p.Id, Name = p.Name, Price = p.Price };

この MapProducts メソッドは IQueryable を返すので、他のクエリ パラメーターを使用して結果を作成できます。 これは、クエリに where 句を追加する GetProduct メソッドでこれを確認できます。

var product = (from p in MapProducts() 
    where p.Id == 1
    select p).FirstOrDefault();

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

次に、ユーザーが注文を作成して表示できるようにするコントローラーを追加します。

別の DTO から始めます。 ソリューション エクスプローラーで、[Models] フォルダーを右クリックし、次の実装を使用する OrderDTO という名前のクラスを追加します。

namespace ProductStore.Models
{
    using System.Collections.Generic;

    public class OrderDTO
    {
        public class Detail
        {
            public int ProductID { get; set; }
            public string Product { get; set; }
            public decimal Price { get; set; }
            public int Quantity { get; set; }
        }
        public IEnumerable<Detail> Details { get; set; }
    }
}

次に、コントローラーを追加します。 ソリューション エクスプローラーで、[コントローラー] フォルダーを右クリックします。 [追加] を選択し、[コントローラー] を選択します。 [コントローラーの追加] ダイアログで、次のオプションを設定します。

  • [コントローラー名] に "OrdersController" と入力します。
  • [テンプレート] で、"Entity Framework を使用した、読み取り/書き込み操作のある API コントローラー" を選択します。
  • Model クラスで、"Order (ProductStore.Models)" を選択します。
  • [データ コンテキスト クラス] で、"OrdersContext (ProductStore.Models)" を選択します。

Screenshot of the add controller dialogue box. OrdersController is written in the text box.

追加をクリックします。 これにより、OrdersController.cs という名前のファイルが追加されます。 次に、コントローラーの既定値の実装を変更する必要があります。

まず、PutOrder メソッドと DeleteOrder メソッドを削除します。 このサンプルでは、顧客は既存の注文を変更または削除することはできません。 実際のアプリケーションでは、このようなケースを処理するために多くのバックエンド ロジックが必要になります。 (例: 注文は既に発送されましたか?)

次のユーザーに属する注文のみを返すように GetOrders メソッドを変更します。

public IEnumerable<Order> GetOrders()
{
    return db.Orders.Where(o => o.Customer == User.Identity.Name);
}

GetOrder メソッドを次のように変更します。

public OrderDTO GetOrder(int id)
{
    Order order = db.Orders.Include("OrderDetails.Product")
        .First(o => o.Id == id && o.Customer == User.Identity.Name);
    if (order == null)
    {
        throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
    }

    return new OrderDTO()
    {
        Details = from d in order.OrderDetails
                  select new OrderDTO.Detail()
                      {
                          ProductID = d.Product.Id,
                          Product = d.Product.Name,
                          Price = d.Product.Price,
                          Quantity = d.Quantity
                      }
    };
}

メソッドに加えた変更を次に示します。

  • 戻り値は OrderDTO インスタンスでは Order ではなくインスタンスです。
  • データベースに注文のクエリを実行するときは、DbQuery.Include メソッドを使用して関連する OrderDetail エンティティと Product エンティティをフェッチします。
  • 投影を使用して結果をフラット化します。

HTTP 応答には、次の数量を含む製品の配列が含まれます。

{"Details":[{"ProductID":1,"Product":"Tomato Soup","Price":1.39,"Quantity":2},
{"ProductID":3,"Product":"Yo yo","Price":6.99,"Quantity":1}]}

この形式は、入れ子になったエンティティ (順序、詳細、製品) を含む元のオブジェクト グラフよりも、クライアントが使用する方が簡単です。

それを PostOrder とみなす最後のメソッド。 現時点では、このメソッドは Order インスタンスを受け取ります。 ただし、クライアントが次のような要求本文を送信した場合はどうなるか考えてみましょう。

{"Customer":"Alice","OrderDetails":[{"Quantity":1,"Product":{"Name":"Koala bears", 
"Price":5,"ActualCost":1}}]}

これは適切に構造化された順序であり、Entity Framework は問題なくデータベースに挿入します。 ただし、以前は存在しなかった Product エンティティが含まれています。 クライアントは、当社のデータベースに新しい製品を作成しました。 コアラ-クマの注文を見たときに、これは注文調達部門にとって予想できない内容になります。 モラルとは、POST 要求または PUT 要求で受け入れるデータに十分に注意することです。

この問題を回避するには、OrderDTO インスタンスを受け取るように PostOrder メソッドを変更します。 OrderDTO を使用して Order を作成します。

var order = new Order()
{
    Customer = User.Identity.Name,
    OrderDetails = (from item in dto.Details select new OrderDetail() 
        { ProductId = item.ProductID, Quantity = item.Quantity }).ToList()
};

ProductID プロパティと Quantity プロパティを使用していることに注意してください。また、クライアントが製品名または価格に対して送信した値は無視されます。 製品 ID が無効な場合、データベースの外部キー制約に違反し、挿入は必ず失敗します。

完全な PostOrder メソッドを次に示します。

public HttpResponseMessage PostOrder(OrderDTO dto)
{
    if (ModelState.IsValid)
    {
        var order = new Order()
        {
            Customer = User.Identity.Name,
            OrderDetails = (from item in dto.Details select new OrderDetail() 
                { ProductId = item.ProductID, Quantity = item.Quantity }).ToList()
        };

        db.Orders.Add(order);
        db.SaveChanges();

        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, order);
        response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = order.Id }));
        return response;
    }
    else
    {
        return Request.CreateResponse(HttpStatusCode.BadRequest);
    }
}

最後に、Authorize 属性をコントローラーに追加します。

[Authorize]
public class OrdersController : ApiController
{
    // ...

これで、登録されたユーザーのみが注文を作成または表示できるようになりました。