Часть 6. Создание контроллеров продуктов и заказов
Рик Андерсон
Добавление контроллера продуктов
Контроллер Администратор предназначен для пользователей с правами администратора. Клиенты, с другой стороны, могут просматривать продукты, но не могут создавать, обновлять или удалять их.
Мы можем легко ограничить доступ к методам Post, Put и Delete, оставляя методы Get открытыми. Но посмотрите на данные, возвращаемые для продукта:
{"Id":1,"Name":"Tomato Soup","Price":1.39,"ActualCost":0.99}
Свойство ActualCost
не должно быть видимым для клиентов! Решение заключается в определении объекта передачи данных (DTO), который включает подмножество свойств, которые должны быть видны клиентам. Мы будем использовать LINQ для проецирования Product
экземпляров в ProductDTO
экземпляры.
Добавьте класс с именем ProductDTO
в папку Models.
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. В Обозреватель решений щелкните правой кнопкой мыши папку 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; }
}
}
Теперь добавьте контроллер. В обозревателе решений щелкните правой кнопкой мыши папку Controllers. Выберите Добавить, а затем — Контроллер. В диалоговом окне Добавление контроллера задайте следующие параметры:
- В разделе Имя контроллера введите "OrdersController".
- В разделе Шаблон выберите "Контроллер API с действиями чтения и записи с помощью Entity Framework".
- В разделе Класс модели выберите "Заказ (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.
Чтобы избежать этой проблемы, измените 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 в контроллер:
[Authorize]
public class OrdersController : ApiController
{
// ...
Теперь только зарегистрированные пользователи могут создавать или просматривать заказы.
Обратная связь
https://aka.ms/ContentUserFeedback.
Ожидается в ближайшее время: в течение 2024 года мы постепенно откажемся от GitHub Issues как механизма обратной связи для контента и заменим его новой системой обратной связи. Дополнительные сведения см. в разделеОтправить и просмотреть отзыв по