作者:里克·安德森
添加产品控制器
管理员控制器适用于具有管理员权限的用户。 另一方面,客户可以查看产品,但无法创建、更新或删除产品。
我们可以轻松地限制对 Post、Put 和 Delete 方法的访问,同时使 Get 方法保持打开状态。 但查看产品返回的数据:
{"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 实例,而是调用 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 开始。 在解决方案资源管理器中,右键单击 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 会愉快地将其插入数据库中。 但它包含以前不存在的产品实体。 客户端刚刚在我们的数据库中创建了一个新产品! 当看到考拉熊的订单时,这将令订单履行部门感到意外。 寓意是,要非常小心你在 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 ,并忽略客户端为产品名称或价格发送的任何值。 如果产品 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
{
// ...
现在,只有已注册的用户才能创建或查看订单。