6부: 제품 및 주문 컨트롤러 만들기
작성자: Rick Anderson
제품 컨트롤러 추가
관리 컨트롤러는 관리자 권한이 있는 사용자를 위한 것입니다. 반면 고객은 제품을 볼 수 있지만 제품을 만들거나 업데이트하거나 삭제할 수는 없습니다.
Get 메서드를 열어 두면서 Post, Put 및 Delete 메서드에 대한 액세스를 쉽게 제한할 수 있습니다. 그러나 제품에 대해 반환되는 데이터를 확인합니다.
{"Id":1,"Name":"Tomato Soup","Price":1.39,"ActualCost":0.99}
이 속성은 ActualCost
고객에게 표시되지 않아야 합니다. 솔루션은 고객에게 표시되어야 하는 속성의 하위 집합을 포함하는 DTO( 데이터 전송 개체 )를 정의하는 것입니다. LINQ를 사용하여 인스턴스를 인스턴스에 ProductDTO
프로젝 Product
팅합니다.
Models 폴더에 라는 ProductDTO
클래스를 추가합니다.
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을 반환하므로 다른 쿼리 매개 변수를 사용하여 결과를 작성할 수 있습니다. 쿼리에 where 절을 GetProduct
추가하는 메서드에서 이를 확인할 수 있습니다.
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"를 입력합니다.
- 템플릿에서 "Entity Framework를 사용하여 읽기/쓰기 작업이 있는 API 컨트롤러"를 선택합니다.
- 모델 클래스에서 "Order(ProductStore.Models)"를 선택합니다.
- 데이터 컨텍스트 클래스에서 "OrdersContext(ProductStore.Models)"를 선택합니다.
추가를 클릭합니다. 그러면 OrdersController.cs라는 파일이 추가됩니다. 다음으로 컨트롤러의 기본 구현을 수정해야 합니다.
먼저 및 DeleteOrder
메서드를 PutOrder
삭제합니다. 이 샘플의 경우 고객은 기존 주문을 수정하거나 삭제할 수 없습니다. 실제 애플리케이션에서는 이러한 경우를 처리하기 위해 많은 백 엔드 논리가 필요합니다. (예를 들어 주문이 이미 배송되었나요?)
사용자에 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
}
};
}
메서드를 변경한 내용은 다음과 같습니다.
- 반환 값은 대신 instance 입니다
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
. 현재 이 메서드는 instance 사용합니다 Order
. 그러나 클라이언트가 다음과 같이 요청 본문을 보내면 어떻게 되는지 고려합니다.
{"Customer":"Alice","OrderDetails":[{"Quantity":1,"Product":{"Name":"Koala bears",
"Price":5,"ActualCost":1}}]}
이는 잘 구성된 순서이며 Entity Framework는 데이터베이스에 삽입합니다. 그러나 이전에는 존재하지 않았던 Product 엔터티가 포함되어 있습니다. 클라이언트는 방금 데이터베이스에 새 제품을 만들었습니다. 이것은 그들이 코알라 곰에 대한 주문을 볼 때 주문 이행 부서에 놀라운 일이 될 것입니다. 도덕적으로 POST 또는 PUT 요청에서 수락하는 데이터에 대해 주의해야 합니다.
이 문제를 방지하려면 메서드를 PostOrder
변경하여 OrderDTO
instance. 를 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()
};
및 Quantity
속성을 사용 ProductID
하며, 클라이언트가 제품 이름 또는 가격에 대해 보낸 모든 값을 무시합니다. 제품 ID가 올바르지 않으면 데이터베이스의 외래 키 제약 조건을 위반하고 삽입이 실패합니다.
전체 메서드는 다음과 같습니다.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
{
// ...
이제 등록된 사용자만 주문을 만들거나 볼 수 있습니다.