ASP.NET Web API 2에서 OData 작업 지원
작성자: Mike Wasson
OData에서 작업은 엔터티에서 CRUD 작업으로 쉽게 정의되지 않는 서버 쪽 동작을 추가하는 방법입니다. 작업에 대한 몇 가지 용도는 다음과 같습니다.
- 복잡한 트랜잭션 구현.
- 여러 엔터티를 한 번에 조작합니다.
- 엔터티의 특정 속성에만 업데이트를 허용합니다.
- 엔터티에 정의되지 않은 서버로 정보 보내기
자습서에서 사용되는 소프트웨어 버전
- Web API 2
- OData 버전 3
- Entity Framework 6
예: 제품 등급 지정
이 예제에서는 사용자가 제품을 평가한 다음 각 제품에 대한 평균 등급을 노출하도록 허용하려고 합니다. 데이터베이스에 제품 키로 지정된 등급 목록을 저장합니다.
Entity Framework의 등급을 나타내는 데 사용할 수 있는 모델은 다음과 같습니다.
public class ProductRating
{
public int ID { get; set; }
[ForeignKey("Product")]
public int ProductID { get; set; }
public virtual Product Product { get; set; } // Navigation property
public int Rating { get; set; }
}
그러나 클라이언트가 개체를 "Ratings" 컬렉션에 게시 ProductRating
하는 것을 원하지 않습니다. 직관적으로 등급은 Products 컬렉션과 연결되며 클라이언트는 등급 값만 게시하면 됩니다.
따라서 일반적인 CRUD 작업을 사용하는 대신 클라이언트가 제품에서 호출할 수 있는 작업을 정의합니다. OData 용어에서 작업은 Product 엔터티에 바인딩 됩니다.
작업에는 서버에 부작용이 있습니다. 이러한 이유로 HTTP POST 요청을 사용하여 호출됩니다. 작업에는 서비스 메타데이터에 설명된 매개 변수 및 반환 형식이 있을 수 있습니다. 클라이언트는 요청 본문에 매개 변수를 보내고 서버는 응답 본문에 반환 값을 보냅니다. "제품 평가" 작업을 호출하기 위해 클라이언트는 다음과 같이 POST를 URI로 보냅니다.
http://localhost/odata/Products(1)/RateProduct
POST 요청의 데이터는 단순히 제품 등급입니다.
{"Rating":2}
엔터티 데이터 모델에서 작업 선언
Web API 구성에서 EDM(엔터티 데이터 모델)에 작업을 추가합니다.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Product>("Products");
builder.EntitySet<Supplier>("Suppliers");
builder.EntitySet<ProductRating>("Ratings");
// New code: Add an action to the EDM, and define the parameter and return type.
ActionConfiguration rateProduct = builder.Entity<Product>().Action("RateProduct");
rateProduct.Parameter<int>("Rating");
rateProduct.Returns<double>();
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}
}
이 코드는 "RateProduct"를 Product 엔터티에서 수행할 수 있는 작업으로 정의합니다. 또한 작업이 "Rating"이라는 int 매개 변수를 사용하고 int 값을 반환한다고 선언합니다.
컨트롤러에 작업 추가
"RateProduct" 작업은 Product 엔터티에 바인딩됩니다. 작업을 구현하려면 Products 컨트롤러에 라는 RateProduct
메서드를 추가합니다.
[HttpPost]
public async Task<IHttpActionResult> RateProduct([FromODataUri] int key, ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
return BadRequest();
}
int rating = (int)parameters["Rating"];
Product product = await db.Products.FindAsync(key);
if (product == null)
{
return NotFound();
}
product.Ratings.Add(new ProductRating() { Rating = rating });
db.SaveChanges();
double average = product.Ratings.Average(x => x.Rating);
return Ok(average);
}
메서드 이름이 EDM의 작업 이름과 일치하는지 확인합니다. 메서드에는 두 개의 매개 변수가 있습니다.
- key: 평가할 제품의 키입니다.
- parameters: 작업 매개 변수 값의 사전입니다.
기본 라우팅 규칙을 사용하는 경우 키 매개 변수의 이름은 "key"여야 합니다. 표시된 것처럼 [FromOdataUri] 특성을 포함하는 것도 중요합니다. 이 특성은 요청 URI에서 키를 구문 분석할 때 OData 구문 규칙을 사용하도록 Web API에 지시합니다.
매개 변수 사전을 사용하여 작업 매개 변수를 가져옵니다.
if (!ModelState.IsValid)
{
return BadRequest();
}
int rating = (int)parameters["Rating"];
클라이언트가 작업 매개 변수를 올바른 형식으로 보내는 경우 ModelState.IsValid 값은 true입니다. 이 경우 ODataActionParameters 사전을 사용하여 매개 변수 값을 가져올 수 있습니다. 이 예제 RateProduct
에서 작업은 "Rating"이라는 단일 매개 변수를 사용합니다.
작업 메타데이터
서비스 메타데이터를 보려면 /odata/$metadata GET 요청을 보냅니다. 다음은 작업을 선언하는 메타데이터의 부분입니다.RateProduct
<FunctionImport Name="RateProduct" m:IsAlwaysBindable="true" IsBindable="true" ReturnType="Edm.Double">
<Parameter Name="bindingParameter" Type="ProductService.Models.Product"/>
<Parameter Name="Rating" Nullable="false" Type="Edm.Int32"/>
</FunctionImport>
FunctionImport 요소는 작업을 선언합니다. 대부분의 필드는 설명이 가능하지만 다음 두 가지에 유의해야 합니다.
- IsBindable 은 적어도 일부 시간 동안 대상 엔터티에서 작업을 호출할 수 있다는 것을 의미합니다.
- IsAlwaysBindable 은 항상 대상 엔터티에서 작업을 호출할 수 있다는 것을 의미합니다.
차이점은 일부 작업은 항상 클라이언트에서 사용할 수 있지만 다른 작업은 엔터티의 상태에 따라 달라질 수 있다는 것입니다. 예를 들어 "구매" 작업을 정의한다고 가정합니다. 재고가 있는 항목만 구입할 수 있습니다. 항목이 품절된 경우 클라이언트는 해당 작업을 호출할 수 없습니다.
EDM을 정의할 때 Action 메서드는 항상 바인딩 가능한 작업을 만듭니다.
builder.Entity<Product>().Action("RateProduct"); // Always bindable
이 항목의 뒷부분에서 항상 바인딩할 수 없는 작업( 일시적 작업이라고도 함)에 대해 설명합니다.
작업 호출
이제 클라이언트가 이 작업을 호출하는 방법을 살펴보겠습니다. 클라이언트가 ID = 4를 사용하여 제품에 2 등급을 부여하려고 하는 경우를 가정해 보겠습니다. 다음은 요청 본문에 JSON 형식을 사용하는 요청 메시지의 예입니다.
POST http://localhost/odata/Products(4)/RateProduct HTTP/1.1
Content-Type: application/json
Content-Length: 12
{"Rating":2}
응답 메시지는 다음과 같습니다.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
DataServiceVersion: 3.0
Date: Tue, 22 Oct 2013 19:04:00 GMT
Content-Length: 89
{
"odata.metadata":"http://localhost:21900/odata/$metadata#Edm.Double","value":2.75
}
엔터티 집합에 작업 바인딩
이전 예제에서 작업은 단일 엔터티에 바인딩됩니다. 클라이언트는 단일 제품을 평가합니다. 작업을 엔터티 컬렉션에 바인딩할 수도 있습니다. 다음을 변경합니다.
EDM에서 엔터티의 Collection 속성에 작업을 추가합니다.
var rateAllProducts = builder.Entity<Product>().Collection.Action("RateAllProducts");
컨트롤러 메서드에서 키 매개 변수를 생략합니다.
[HttpPost]
public int RateAllProducts(ODataActionParameters parameters)
{
// ....
}
이제 클라이언트는 Products 엔터티 집합에 대한 작업을 호출합니다.
http://localhost/odata/Products/RateAllProducts
컬렉션 매개 변수가 있는 작업
작업에는 값 컬렉션을 사용하는 매개 변수가 있을 수 있습니다. EDM에서 CollectionParameter<T> 를 사용하여 매개 변수를 선언합니다.
rateAllProducts.CollectionParameter<int>("Ratings");
int 값 컬렉션을 사용하는 "Ratings"라는 매개 변수를 선언합니다. 컨트롤러 메서드에서는 ODataActionParameters 개체에서 매개 변수 값을 계속 가져올 수 있지만 이제 값은 ICollection<int> 값입니다.
[HttpPost]
public void RateAllProducts(ODataActionParameters parameters)
{
if (!ModelState.IsValid)
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
var ratings = parameters["Ratings"] as ICollection<int>;
// ...
}
임시 작업
"RateProduct" 예제에서 사용자는 항상 제품을 평가할 수 있으므로 작업을 항상 사용할 수 있습니다. 그러나 일부 작업은 엔터티의 상태에 따라 달라집니다. 예를 들어 비디오 대여 서비스에서 "CheckOut" 작업을 항상 사용할 수 있는 것은 아닙니다. (해당 비디오의 복사본을 사용할 수 있는지 여부에 따라 달라집니다.) 이 유형의 작업을 임시 작업이라고 합니다.
서비스 메타데이터에서 일시적인 작업에 는 IsAlwaysBindable이 false와 같습니다. 이는 실제로 기본값이므로 메타데이터는 다음과 같습니다.
<FunctionImport Name="CheckOut" IsBindable="true">
<Parameter Name="bindingParameter" Type="ProductsService.Models.Product" />
</FunctionImport>
이것이 중요한 이유는 다음과 같습니다. 작업이 일시적인 경우 서버는 작업을 사용할 수 있을 때 클라이언트에 알려야 합니다. 엔터티에 작업에 대한 링크를 포함하여 이 작업을 수행합니다. 다음은 Movie 엔터티의 예입니다.
{
"odata.metadata":"http://localhost:17916/odata/$metadata#Movies/@Element",
"#CheckOut":{ "target":"http://localhost:17916/odata/Movies(1)/CheckOut" },
"ID":1,"Title":"Sudden Danger 3","Year":2012,"Genre":"Action"
}
"#CheckOut" 속성에는 CheckOut 작업에 대한 링크가 포함되어 있습니다. 작업을 사용할 수 없는 경우 서버는 링크를 생략합니다.
EDM에서 임시 작업을 선언하려면 TransientAction 메서드를 호출합니다.
var checkoutAction = builder.Entity<Movie>().TransientAction("CheckOut");
또한 지정된 엔터티에 대한 작업 링크를 반환하는 함수를 제공해야 합니다. HasActionLink를 호출하여 이 함수를 설정합니다. 함수를 람다 식으로 작성할 수 있습니다.
checkoutAction.HasActionLink(ctx =>
{
var movie = ctx.EntityInstance as Movie;
if (movie.IsAvailable) {
return new Uri(ctx.Url.ODataLink(
new EntitySetPathSegment(ctx.EntitySet),
new KeyValuePathSegment(movie.ID.ToString()),
new ActionPathSegment(checkoutAction.Name)));
}
else
{
return null;
}
}, followsConventions: true);
작업을 사용할 수 있는 경우 람다 식은 작업에 대한 링크를 반환합니다. OData serializer는 엔터티를 직렬화할 때 이 링크를 포함합니다. 작업을 사용할 수 없는 경우 함수는 를 반환합니다 null
.