Часть 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 записывается в текстовое поле.

Нажмите кнопку Добавить. При этом будет добавлен файл с именем 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
{
    // ...

Теперь только зарегистрированные пользователи могут создавать или просматривать заказы.