Часть 6. Создание контроллеров продуктов и заказовPart 6: Creating Product and Order Controllers

по Майк Уоссонby Mike Wasson

Скачать завершенный проектDownload Completed Project

Добавление контроллера продуктовAdd a Products Controller

Административный контроллер предназначен для пользователей с правами администратора.The Admin controller is for users who have administrator privileges. Клиенты, с другой стороны, могут просматривать продукты, но не могут создавать, обновлять или удалять их.Customers, on the other hand, can view products but cannot create, update, or delete them.

Можно легко ограничить доступ к методам POST, WHERE и DELETE, не открывая методы Get.We can easily restrict access to the Post, Put, and Delete methods, while leaving the Get methods open. Но взгляните на данные, возвращаемые для продукта:But look at the data that is returned for a product:

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

Свойство ActualCost не должно быть видимым для клиентов!The ActualCost property should not be visible to customers! Решение заключается в определении объекта передачи данных (DTO), который включает подмножество свойств, которые должны быть видимыми для клиентов.The solution is to define a data transfer object (DTO) that includes a subset of properties that should be visible to customers. Для ProductDTO экземпляров мы будем использовать LINQ to Project Product Instances.We will use LINQ to project Product instances to ProductDTO instances.

Добавьте класс с именем ProductDTO в папку Models.Add a class named ProductDTO to the Models folder.

namespace ProductStore.Models
{
    public class ProductDTO
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

Теперь добавьте контроллер.Now add the controller. В обозреватель решений щелкните правой кнопкой мыши папку Controllers.In Solution Explorer, right-click the Controllers folder. Нажмите кнопку Добавить, а затем выберите контроллер.Select Add, then select Controller. В диалоговом окне Добавление контроллера введите имя контроллера "продуктсконтроллер".In the Add Controller dialog, name the controller "ProductsController". В разделе шаблонвыберите пустой контроллер API.Under Template, select Empty API controller.

Замените все в исходном файле следующим кодом:Replace everything in the source file with the following code:

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 для запроса к базе данных.The controller still uses the OrdersContext to query the database. Но вместо того, чтобы возвращать экземпляры Product напрямую, мы вызываем MapProducts для проецирования их на экземпляры ProductDTO:But instead of returning Product instances directly, we call MapProducts to project them onto ProductDTO instances:

return from p in db.Products select new ProductDTO() 
    { Id = p.Id, Name = p.Name, Price = p.Price };

Метод MapProducts возвращает IQueryable, поэтому мы можем составить результат с другими параметрами запроса.The MapProducts method returns an IQueryable, so we can compose the result with other query parameters. Это можно увидеть в методе GetProduct, который добавляет к запросу предложение WHERE :You can see this in the GetProduct method, which adds a where clause to the query:

var product = (from p in MapProducts() 
    where p.Id == 1
    select p).FirstOrDefault();

Добавление контроллера заказовAdd an Orders Controller

Затем добавьте контроллер, позволяющий пользователям создавать и просматривать заказы.Next, add a controller that lets users create and view orders.

Мы начнем с другого DTO.We'll start with another DTO. В обозреватель решений щелкните правой кнопкой мыши папку Models и добавьте класс с именем OrderDTO использовать следующую реализацию:In Solution Explorer, right-click the Models folder and add a class named OrderDTO Use the following implementation:

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

Теперь добавьте контроллер.Now add the controller. В обозреватель решений щелкните правой кнопкой мыши папку Controllers.In Solution Explorer, right-click the Controllers folder. Нажмите кнопку Добавить, а затем выберите контроллер.Select Add, then select Controller. В диалоговом окне Добавление контроллера задайте следующие параметры.In the Add Controller dialog, set the following options:

  • В разделе имя контроллеравведите "ордерсконтроллер".Under Controller Name, enter "OrdersController".
  • В разделе шаблонвыберите "контроллер API с действиями чтения и записи, используя Entity Framework".Under Template, select "API controller with read/write actions, using Entity Framework".
  • В разделе класс моделивыберите"порядок "(Продуктсторе. Models).Under Model class, select "Order (ProductStore.Models)".
  • В разделе класс контекста данныхвыберите "Ордерсконтекст (Продуктсторе. Models)".Under Data context class, select "OrdersContext (ProductStore.Models)".

Нажмите кнопку Добавить.Click Add. При этом добавляется файл с именем OrdersController.cs.This adds a file named OrdersController.cs. Далее необходимо изменить реализацию контроллера по умолчанию.Next, we need to modify the default implementation of the controller.

Сначала удалите методы PutOrder и DeleteOrder.First, delete the PutOrder and DeleteOrder methods. В этом примере клиенты не могут изменять или удалять существующие заказы.For this sample, customers cannot modify or delete existing orders. В реальных приложениях для обработки таких случаев потребуется много серверной логики.In a real application, you would need lots of back-end logic to handle these cases. (Например, был ли уже отгружен заказ?)(For example, was the order already shipped?)

Измените метод GetOrders, чтобы возвращались только заказы, принадлежащие пользователю.Change the GetOrders method to return just the orders that belong to the user:

public IEnumerable<Order> GetOrders()
{
    return db.Orders.Where(o => o.Customer == User.Identity.Name);
}

Измените метод GetOrder следующим образом:Change the GetOrder method as follows:

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

Ниже приведены изменения, внесенные в метод.Here are the changes that we made to the method:

  • Возвращаемое значение — это OrderDTO экземпляр, а не Order.The return value is an OrderDTO instance, instead of an Order.
  • При запросе базы данных для заказа мы используем метод дбкуери. include для получения связанных OrderDetail и Product сущностей.When we query the database for the order, we use the DbQuery.Include method to fetch the related OrderDetail and Product entities.
  • Мы будем сводить результаты с помощью проекции.We flatten the result by using a projection.

HTTP-ответ будет содержать массив продуктов с количествами:The HTTP response will contain an array of products with quantities:

{"Details":[{"ProductID":1,"Product":"Tomato Soup","Price":1.39,"Quantity":2},
{"ProductID":3,"Product":"Yo yo","Price":6.99,"Quantity":1}]}

Этот формат легче использовать для клиентов, чем исходный граф объектов, который содержит вложенные сущности (Order, Details и Products).This format is easier for clients to consume than the original object graph, which contains nested entities (order, details, and products).

Последний метод, который следует рассмотреть PostOrder.The last method to consider it PostOrder. Сейчас этот метод принимает Order экземпляр.Right now, this method takes an Order instance. Но рассмотрим, что произойдет, если клиент отправляет текст запроса следующим образом:But consider what happens if a client sends a request body like this:

{"Customer":"Alice","OrderDetails":[{"Quantity":1,"Product":{"Name":"Koala bears", 
"Price":5,"ActualCost":1}}]}

Это хорошо структурированный порядок, который Entity Framework, в своюмся случае, будет куда-то вставить в базу данных.This is a well-structured order, and Entity Framework will happily insert it into the database. Но она содержит сущность Product, которая ранее не существовала.But it contains a Product entity that did not exist previously. Клиент только что создал новый продукт в нашей базе данных!The client just created a new product in our database! Это будет неожиданным для отдела выполнения заказов, когда он увидит заказ на Koala.This will be a surprise to the order fulfillment department, when they see an order for koala bears. Мораль — это, по сути, тщательный анализ данных, принимаемых в запросе POST или Request.The moral is, be really careful about the data you accept in a POST or PUT request.

Чтобы избежать этой проблемы, измените метод PostOrder для получения экземпляра OrderDTO.To avoid this problem, change the PostOrder method to take an OrderDTO instance. Для создания Orderиспользуйте OrderDTO.Use the OrderDTO to create the 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 и будем пропускать все значения, отправленные клиентом по названию или цене продукта.Notice that we use the ProductID and Quantity properties, and we ignore any values that the client sent for either product name or price. Если идентификатор продукта недействителен, он нарушает ограничение внешнего ключа в базе данных, и вставка завершится ошибкой, как это должно произойти.If the product ID is not valid, it will violate the foreign key constraint in the database, and the insert will fail, as it should.

Ниже приведен полный метод PostOrder.Here is the complete PostOrder method:

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

Наконец, добавьте к контроллеру атрибут авторизации :Finally, add the Authorize attribute to the controller:

[Authorize]
public class OrdersController : ApiController
{
    // ...

Теперь только зарегистрированные пользователи могут создавать или просматривать заказы.Now only registered users can create or view orders.