第 6 部分:建立產品和訂單控制器
作者:Rick Anderson
新增產品控制器
管理員控制器適用於具有管理員特殊權限的使用者。 另一方面,客戶可以檢視產品,但無法建立、更新或刪除產品。
我們可以輕鬆地限制對 Post、Put 和 Delete 方法的存取,同時讓 Get 方法保持開啟狀態。 但請查看針對產品傳回的資料:
{"Id":1,"Name":"Tomato Soup","Price":1.39,"ActualCost":0.99}
客戶不應該看到 ActualCost
屬性! 解決方案是定義資料傳輸物件 (DTO),其中包含客戶應該看見的屬性子集。 我們將使用 LINQ 將 Product
執行個體投影到 ProductDTO
執行個體。
將命名為 ProductDTO
的類別新增至 [模型] 資料夾。
namespace ProductStore.Models
{
public class ProductDTO
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
現在新增控制器。 在方案總管中,以滑鼠右鍵按一下 Controllers 資料夾。 選取 [新增],然後選取 [控制器]。 在 [新增控制器] 對話方塊中,將控制器命名為「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
執行個體,而是呼叫 MapProducts
將其投影到 ProductDTO
執行個體上:
return from p in db.Products select new ProductDTO()
{ Id = p.Id, Name = p.Name, Price = p.Price };
MapProducts
方法會傳回 IQueryable,因此我們可以使用其他查詢參數撰寫結果。 您可以在 GetProduct
方法中看到此,這會將 where 子句新增至查詢:
var product = (from p in MapProducts()
where p.Id == 1
select p).FirstOrDefault();
新增訂單控制器
接下來,新增可讓使用者建立和檢視訂單的控制器。
我們將從另一個 DTO 開始。 在 [方案總管] 中,以滑鼠右鍵按一下 [模型] 資料夾,然後新增名為 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; }
}
}
現在新增控制器。 在方案總管中,以滑鼠右鍵按一下 Controllers 資料夾。 選取 [新增],然後選取 [控制器]。 在 [新增控制器] 對話方塊中,設定下列選項:
- 在 [控制器名稱] 下,輸入「OrdersController」。
- 在 [範本],選取 [使用 Entity Framework,具有讀取/寫入動作的 API 控制器]。
- 在 [模型類別] 下,選取 [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 會順利地將其插入資料庫中。 但它包含先前不存在的產品實體。 用戶端剛剛在我們的資料庫中建立了新產品! 當他們看到無尾熊的訂單時,這會讓訂單履行部門感到意外。 重點是請在 POST 或 PUT 要求中接受資料時格外小心。
若要避免這個問題,請將 PostOrder
方法變更為採用 OrderDTO
執行個體。 使用 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
屬性,而我們會忽略用戶端針對產品名稱或價格傳送的任何值。 如果產品識別碼無效,它會違反資料庫中的外部索引鍵條件約束,而且插入將會失敗,理應如此。
以下是完整的 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]
public class OrdersController : ApiController
{
// ...
現在只有已註冊的使用者可以建立或檢視訂單。