Parte 6: Creación de controladores de productos y pedidos

Por Rick Anderson

Descargar el proyecto completado

Agregar un controlador de productos

El controlador de administración es para los usuarios que tienen privilegios de administrador. Por otro lado, los clientes pueden ver los productos, pero no pueden crearlos, actualizarlos ni eliminarlos.

Podemos restringir fácilmente el acceso a los métodos Post, Put y Delete, mientras se deja abiertos los métodos Get. Pero examine los datos que se devuelven para un producto:

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

La propiedad ActualCost no debe ser visible para los clientes. La solución consiste en definir un objeto de transferencia de datos (DTO) que incluya un subconjunto de propiedades que deben ser visibles para los clientes. Usaremos LINQ para proyectar instancias de Product en instancias de ProductDTO.

Agregue una clase denominada ProductDTO a la carpeta Models.

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

Ahora agregue el controlador. En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Controllers. Seleccione Agregar y, a continuación, seleccione Controlador. En el cuadro de diálogo Agregar controlador, asigne al controlador el nombre "ProductsController". En Plantilla, seleccione Controlador de API vacío.

Screenshot of the add controller dialogue box.

Reemplace todo en el archivo de código fuente por el código siguiente:

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

El controlador sigue usando OrdersContext para consultar la base de datos. Pero en lugar de devolver instancias Product directamente, llamamos a MapProducts para proyectarlas en instancias ProductDTO:

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

El método MapProducts devuelve un IQueryable, por lo que podemos componer el resultado con otros parámetros de consulta. Puede ver esto en el método GetProduct, lo que agrega una cláusula where a la consulta:

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

Agregar un controlador de pedidos

A continuación, agregue un controlador que permita a los usuarios crear y ver pedidos.

Comenzaremos con otro DTO. En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Models y agregue una clase denominada OrderDTO. Use la siguiente implementación:

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

Ahora agregue el controlador. En el Explorador de soluciones, haga clic con el botón derecho en la carpeta Controllers. Seleccione Agregar y, a continuación, seleccione Controlador. En el cuadro de diálogo Agregar controlador, establezca las siguientes opciones:

  • En Nombre del controlador, escriba "OrdersController".
  • En Plantilla, seleccione "API controller with read/write actions, using Entity Framework".
  • En Clase modelo, seleccione "Order (ProductStore.Models)".
  • En Clase de contexto de datos, seleccione "OrdersContext (ProductStore.Models)".

Screenshot of the add controller dialogue box. OrdersController is written in the text box.

Haga clic en Agregar. Esto agrega un archivo denominado OrdersController.cs. A continuación, es necesario modificar la implementación predeterminada del controlador.

En primer lugar, elimine los métodos PutOrder y DeleteOrder. Para este ejemplo, los clientes no pueden modificar ni eliminar pedidos existentes. En una aplicación real, necesitaría una gran cantidad de lógica de back-end para controlar estos casos. (Por ejemplo, ¿el pedido ya se envió?)

Cambie el método GetOrders para devolver solo los pedidos que pertenecen al usuario:

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

Cambie el método GetOrder de la siguiente manera:

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

Estos son los cambios realizados en el método:

  • El valor devuelto es una instancia de OrderDTO en lugar de Order.
  • Cuando consultamos la base de datos para el pedido, usamos el método DbQuery.Include para capturar las entidades OrderDetail y Product relacionadas.
  • Se aplana el resultado mediante una proyección.

La respuesta HTTP contendrá una matriz de productos con cantidades:

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

Este formato es más fácil de consumir para los clientes que el gráfico de objetos original, que contiene entidades anidadas (pedido, detalles y productos).

El último método que se debe tener en cuenta es PostOrder. En este momento, este método toma una instancia Order. Pero tenga en cuenta lo que sucede si un cliente envía un cuerpo de solicitud de la siguiente manera:

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

Se trata de un pedido bien estructurado y Entity Framework lo insertará correctamente en la base de datos. Pero contiene una entidad Product que no existía anteriormente. El cliente acaba de crear un nuevo producto en nuestra base de datos. Esto será una sorpresa para el departamento de cumplimiento de pedidos cuando vean un pedido de koalas. La moraleja es que tenga mucho cuidado con los datos que acepta en una solicitud POST o PUT.

Para evitar este problema, cambie el método PostOrder para tomar una instancia OrderDTO. Use OrderDTO para crear 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()
};

Tenga en cuenta que usamos las propiedades ProductID y Quantity, y omitimos los valores que el cliente envió para el nombre del producto o el precio. Si el id. del producto no es válido, infringirá la restricción de clave externa en la base de datos y se producirá un error en la inserción, como debería.

Este es el método completo 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);
    }
}

Por último, agregue el atributo Authorize al controlador:

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

Ahora solo los usuarios registrados pueden crear o ver pedidos.