Parte 6: Creazione di controller di prodotto e ordine
di Rick Anderson
Scaricare il progetto completato
Aggiungere un controller di prodotti
Il controller di Amministrazione è destinato agli utenti con privilegi di amministratore. I clienti, invece, possono visualizzare i prodotti, ma non possono creare, aggiornare o eliminarli.
È possibile limitare facilmente l'accesso ai metodi Post, Put ed Delete, lasciando aperti i metodi Get. Tuttavia, esaminare i dati restituiti per un prodotto:
{"Id":1,"Name":"Tomato Soup","Price":1.39,"ActualCost":0.99}
La ActualCost
proprietà non deve essere visibile ai clienti! La soluzione consiste nel definire un oggetto DTO ( Data Transfer Object ) che include un subset di proprietà che devono essere visibili ai clienti. Verrà usato LINQ per proiettare Product
le istanze alle ProductDTO
istanze.
Aggiungere una classe denominata ProductDTO
alla cartella Models.
namespace ProductStore.Models
{
public class ProductDTO
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
}
Aggiungere ora il controller. In Esplora soluzioni fare clic sulla cartella Controller. Selezionare Aggiungi e quindi Controller. Nella finestra di dialogo Aggiungi controller assegnare al controller il nome "ProductsController". In Modello selezionare Controller API vuoto.
Sostituire tutto nel file di origine con il codice seguente:
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);
}
}
}
Il controller usa comunque l'oggetto OrdersContext
per eseguire query sul database. Invece di restituire Product
direttamente le istanze, viene chiamato MapProducts
a proiettarli in ProductDTO
istanze:
return from p in db.Products select new ProductDTO()
{ Id = p.Id, Name = p.Name, Price = p.Price };
Il MapProducts
metodo restituisce un oggetto IQueryable, in modo da poter comporre il risultato con altri parametri di query. Questo è possibile vedere nel GetProduct
metodo, che aggiunge una clausola where alla query:
var product = (from p in MapProducts()
where p.Id == 1
select p).FirstOrDefault();
Aggiungere un controller ordini
Aggiungere quindi un controller che consente agli utenti di creare e visualizzare gli ordini.
Inizieremo con un altro DTO. In Esplora soluzioni fare clic con il pulsante destro del mouse sulla cartella Modelli e aggiungere una classe denominata OrderDTO
Usa l'implementazione seguente:
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; }
}
}
Aggiungere ora il controller. In Esplora soluzioni fare clic sulla cartella Controller. Selezionare Aggiungi e quindi Controller. Nella finestra di dialogo Aggiungi controller impostare le opzioni seguenti:
- In Nome controller immettere "OrdersController".
- In Modello selezionare "Controller API con azioni di lettura/scrittura, usando Entity Framework".
- In Classe Model selezionare "Order (ProductStore.Models)".
- In Classe contesto dati selezionare "OrdersContext (ProductStore.Models)".
Scegliere Aggiungi. In questo modo viene aggiunto un file denominato OrdersController.cs. È quindi necessario modificare l'implementazione predefinita del controller.
Prima di tutto, eliminare i PutOrder
metodi e DeleteOrder
. Per questo esempio, i clienti non possono modificare o eliminare ordini esistenti. In un'applicazione reale è necessario un sacco di logica back-end per gestire questi casi. (Ad esempio, è stato già spedito l'ordine?)
Modificare il GetOrders
metodo per restituire solo gli ordini appartenenti all'utente:
public IEnumerable<Order> GetOrders()
{
return db.Orders.Where(o => o.Customer == User.Identity.Name);
}
Modificare il GetOrder
metodo come indicato di seguito:
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
}
};
}
Ecco le modifiche apportate al metodo:
- Il valore restituito è un'istanza
OrderDTO
, anziché unOrder
oggetto . - Quando si esegue una query sul database per l'ordine, viene usato il metodo DbQuery.Include per recuperare le entità e
Product
correlateOrderDetail
. - Il risultato viene appiattito usando una proiezione.
La risposta HTTP conterrà una matrice di prodotti con quantità:
{"Details":[{"ProductID":1,"Product":"Tomato Soup","Price":1.39,"Quantity":2},
{"ProductID":3,"Product":"Yo yo","Price":6.99,"Quantity":1}]}
Questo formato è più semplice per i client da usare rispetto al grafico degli oggetti originale, che contiene entità annidate (ordine, dettagli e prodotti).
Ultimo metodo da considerare PostOrder
. A questo momento, questo metodo accetta un'istanza Order
. Si consideri tuttavia cosa accade se un client invia un corpo della richiesta simile al seguente:
{"Customer":"Alice","OrderDetails":[{"Quantity":1,"Product":{"Name":"Koala bears",
"Price":5,"ActualCost":1}}]}
Si tratta di un ordine ben strutturato e Entity Framework lo inserisce felicemente nel database. Contiene tuttavia un'entità Product che non esiste in precedenza. Il client ha appena creato un nuovo prodotto nel database! Questa sarà una sorpresa per il reparto di evasione degli ordini, quando vedono un ordine per gli orsi koala. La morale è, prestare molta attenzione ai dati accettati in una richiesta POST o PUT.
Per evitare questo problema, modificare il metodo per accettare un'istanza PostOrder
OrderDTO
. Usare l'oggetto OrderDTO
per creare l'oggetto 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()
};
Si noti che vengono usate le proprietà e Quantity
e vengono ignorati tutti i ProductID
valori inviati dal client per il nome del prodotto o il prezzo. Se l'ID prodotto non è valido, viola il vincolo di chiave esterna nel database e l'inserimento avrà esito negativo, come dovrebbe.
Ecco il metodo 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);
}
}
Infine, aggiungere l'attributo Authorize al controller:
[Authorize]
public class OrdersController : ApiController
{
// ...
Ora solo gli utenti registrati possono creare o visualizzare gli ordini.