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.
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)".
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 deOrder
. - Cuando consultamos la base de datos para el pedido, usamos el método DbQuery.Include para capturar las entidades
OrderDetail
yProduct
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.