第 6 部分:创建产品和订单控制器

作者:Rick Anderson

下载已完成项目

添加产品控制器

管理员控制器适用于具有管理员权限的用户。 另一方面,客户可以查看产品,但不能创建、更新或删除产品。

我们可以轻松地限制对 Post、Put 和 Delete 方法的访问,同时使 Get 方法保持打开状态。 但请查看为产品返回的数据:

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

ActualCost 属性不应对客户可见! 解决方案是定义一个 数据传输对象 (DTO) ,其中包括客户应看到的属性子集。 我们将使用 LINQ 将实例投影 ProductProductDTO 实例。

将名为 的 ProductDTO 类添加到 Models 文件夹。

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 来查询数据库。 但是,我们调用 MapProducts 来将它们投影到 实例上ProductDTO,而不是直接返回Product实例:

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 开始。 在 解决方案资源管理器,右键单击 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”。
  • “模板”下,选择“使用实体框架执行读/写操作的 API 控制器”。
  • Model 类下,选择“订购 (ProductStore.Models) ”。
  • “Data 上下文类”下,选择“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}}]}

这是一个结构良好的顺序,实体框架会很乐意将其插入数据库。 但它包含以前不存在的 Product 实体。 客户端刚刚在我们的数据库中创建了一个新产品! 当他们看到考拉熊的订单时,订单履行部门会感到惊讶。 道德是,请务必小心你在 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 属性,并且忽略客户端为产品名称或价格发送的任何值。 如果产品 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
{
    // ...

现在只有已注册的用户才能创建或查看订单。