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.

추가 리소스