パート 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 コントローラー] を選択します。
ソース ファイル内のすべてを、次のコードに置き換えます。
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)" を選択します。
追加をクリックします。 これにより、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
{
// ...
これで、登録されたユーザーのみが注文を作成または表示できるようになりました。