共用方式為


第 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 會寫入文字方塊中。

按一下新增。 這會新增名為 OrdersController.cs 的檔案。 接下來,我們需要修改控制器的預設實作。

首先,刪除 PutOrderDeleteOrder 方法。 在此範例中,客戶無法修改或刪除現有的訂單。 在實際的應用程式中,您需要大量的後端邏輯來處理這些案例。 (例如,訂單是否已出貨?)

變更 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 方法來擷取相關 OrderDetailProduct 實體。
  • 我們會使用投影來壓平結果。

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

請注意,我們使用 ProductIDQuantity 屬性,而我們會忽略用戶端針對產品名稱或價格傳送的任何值。 如果產品識別碼無效,它會違反資料庫中的外部索引鍵條件約束,而且插入將會失敗,理應如此。

以下是完整的 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
{
    // ...

現在只有已註冊的使用者可以建立或檢視訂單。