OData v4에서 ASP.NET Web API 2.2를 사용하는 엔터티 관계

작성자: Mike Wasson

대부분의 데이터 세트는 엔터티 간의 관계를 정의합니다. 고객은 주문이 있습니다. 책에는 저자가 있습니다. 제품에는 공급업체가 있습니다. OData를 사용하여 클라이언트는 엔터티 관계를 탐색할 수 있습니다. 제품이 제공되면 공급업체를 찾을 수 있습니다. 관계를 만들거나 제거할 수도 있습니다. 예를 들어 제품에 대한 공급자를 설정할 수 있습니다.

이 자습서에서는 ASP.NET Web API 사용하여 OData v4에서 이러한 작업을 지원하는 방법을 보여 줍니다. 이 자습서는 ASP.NET Web API 2를 사용하여 OData v4 엔드포인트 만들기 자습서를 기반으로 합니다.

자습서에서 사용되는 소프트웨어 버전

  • Web API 2.1
  • OData v4
  • Visual Studio 2017( 여기에서 Visual Studio 2017 다운로드)
  • Entity Framework 6
  • .NET 4.5

자습서 버전

OData 버전 3의 경우 OData v3에서 엔터티 관계 지원을 참조하세요.

공급업체 엔터티 추가

참고

이 자습서는 ASP.NET Web API 2를 사용하여 OData v4 엔드포인트 만들기 자습서를 기반으로 합니다.

먼저 관련 엔터티가 필요합니다. Models 폴더에 라는 Supplier 클래스를 추가합니다.

using System.Collections.Generic;

namespace ProductService.Models
{
    public class Supplier
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public ICollection<Product> Products { get; set; }
    }
}

클래스에 탐색 속성을 Product 추가합니다.

using System.ComponentModel.DataAnnotations.Schema;

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }

        // New code:    
        [ForeignKey("Supplier")]
        public int? SupplierId { get; set; }
        public virtual Supplier Supplier { get; set; }
    }
}

Entity Framework가 데이터베이스에 Supplier 테이블을 포함하도록 ProductsContext 클래스에 새 DbSet를 추가합니다.

public class ProductsContext : DbContext
{
    static ProductsContext()
    {
        Database.SetInitializer(new ProductInitializer());
    }

    public DbSet<Product> Products { get; set; }
    // New code:
    public DbSet<Supplier> Suppliers { get; set; }
}

WebApiConfig.cs에서 엔터티 데이터 모델에 "Suppliers" 엔터티 집합을 추가합니다.

public static void Register(HttpConfiguration config)
{
    ODataModelBuilder builder = new ODataConventionModelBuilder();
    builder.EntitySet<Product>("Products");
    // New code:
    builder.EntitySet<Supplier>("Suppliers");
    config.MapODataServiceRoute("ODataRoute", null, builder.GetEdmModel());
}

Suppliers 컨트롤러 추가

SuppliersController Controllers 폴더에 클래스를 추가합니다.

using ProductService.Models;
using System.Linq;
using System.Web.OData;

namespace ProductService.Controllers
{
    public class SuppliersController : ODataController
    {
        ProductsContext db = new ProductsContext();

        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

이 컨트롤러에 대한 CRUD 작업을 추가하는 방법을 표시하지 않습니다. 단계는 Products 컨트롤러와 동일합니다( OData v4 엔드포인트 만들기 참조).

제품에 대한 공급업체를 가져오기 위해 클라이언트는 GET 요청을 보냅니다.

GET /Products(1)/Supplier

이 요청을 지원하려면 클래스에 다음 메서드를 ProductsController 추가합니다.

public class ProductsController : ODataController
{
    // GET /Products(1)/Supplier
    [EnableQuery]
    public SingleResult<Supplier> GetSupplier([FromODataUri] int key)
    {
        var result = db.Products.Where(m => m.Id == key).Select(m => m.Supplier);
        return SingleResult.Create(result);
    }
 
   // Other controller methods not shown.
}

이 메서드는 기본 명명 규칙을 사용합니다.

  • 메서드 이름: GetX, 여기서 X는 탐색 속성입니다.
  • 매개 변수 이름:

이 명명 규칙을 따르는 경우 Web API는 자동으로 HTTP 요청을 컨트롤러 메서드에 매핑합니다.

HTTP 요청 예제:

GET http://myproductservice.example.com/Products(1)/Supplier HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 응답 예제:

HTTP/1.1 200 OK
Content-Length: 125
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 00:44:27 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Suppliers/$entity","Id":2,"Name":"Wingtip Toys"
}

이전 예제에서 제품에는 공급자가 하나 있습니다. 탐색 속성은 컬렉션을 반환할 수도 있습니다. 다음 코드는 공급업체에 대한 제품을 가져옵니다.

public class SuppliersController : ODataController
{
    // GET /Suppliers(1)/Products
    [EnableQuery]
    public IQueryable<Product> GetProducts([FromODataUri] int key)
    {
        return db.Suppliers.Where(m => m.Id.Equals(key)).SelectMany(m => m.Products);
    }

    // Other controller methods not shown.
}

이 경우 메서드는 SingleResult<T> 대신 IQueryable을 반환합니다.

HTTP 요청 예제:

GET http://myproductservice.example.com/Suppliers(2)/Products HTTP/1.1
User-Agent: Fiddler
Host: myproductservice.example.com

HTTP 응답 예제:

HTTP/1.1 200 OK
Content-Length: 372
Content-Type: application/json; odata.metadata=minimal; odata.streaming=true
Server: Microsoft-IIS/8.0
OData-Version: 4.0
Date: Tue, 08 Jul 2014 01:06:54 GMT

{
  "@odata.context":"http://myproductservice.example.com/$metadata#Products","value":[
    {
      "Id":1,"Name":"Hat","Price":14.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":2,"Name":"Socks","Price":6.95,"Category":"Clothing","SupplierId":2
    },{
      "Id":4,"Name":"Pogo Stick","Price":29.99,"Category":"Toys","SupplierId":2
    }
  ]
}

엔터티 간 관계 만들기

OData는 두 기존 엔터티 간의 관계를 만들거나 제거할 수 있습니다. OData v4 용어에서 관계는 "참조"입니다. (OData v3에서는 관계를 링크라고 했습니다. 이 자습서에서는 프로토콜 차이점이 중요하지 않습니다.)

참조에는 형식 /Entity/NavigationProperty/$ref이 인 자체 URI가 있습니다. 예를 들어 제품과 공급자 간의 참조를 처리하는 URI는 다음과 같습니다.

http:/host/Products(1)/Supplier/$ref

관계를 추가하기 위해 클라이언트는 POST 또는 PUT 요청을 이 주소로 보냅니다.

  • 탐색 속성이 와 같은 Product.Supplier단일 엔터티인 경우 PUT
  • 탐색 속성이 컬렉션인 경우 POST(예: Supplier.Products).

요청 본문에는 관계에 있는 다른 엔터티의 URI가 포함됩니다. 다음은 요청 예제입니다.

PUT http://myproductservice.example.com/Products(6)/Supplier/$ref HTTP/1.1
OData-Version: 4.0;NetFx
OData-MaxVersion: 4.0;NetFx
Accept: application/json;odata.metadata=minimal
Accept-Charset: UTF-8
Content-Type: application/json;odata.metadata=minimal
User-Agent: Microsoft ADO.NET Data Services
Host: myproductservice.example.com
Content-Length: 70
Expect: 100-continue

{"@odata.id":"http://myproductservice.example.com/Suppliers(4)"}

이 예제에서 클라이언트는 ID = 6인 제품의 에 대한 Supplier $ref URI인 PUT 요청을 /Products(6)/Supplier/$ref에 보냅니다. 요청이 성공하면 서버는 204(콘텐츠 없음) 응답을 보냅니다.

HTTP/1.1 204 No Content
Server: Microsoft-IIS/8.0
Date: Tue, 08 Jul 2014 06:35:59 GMT

다음은 에 관계를 추가하는 컨트롤러 메서드입니다.Product

public class ProductsController : ODataController
{
    [AcceptVerbs("POST", "PUT")]
    public async Task<IHttpActionResult> CreateRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = await db.Products.SingleOrDefaultAsync(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }
        switch (navigationProperty)
        {
            case "Supplier":
                // Note: The code for GetKeyFromUri is shown later in this topic.
                var relatedKey = Helpers.GetKeyFromUri<int>(Request, link);
                var supplier = await db.Suppliers.SingleOrDefaultAsync(f => f.Id == relatedKey);
                if (supplier == null)
                {
                    return NotFound();
                }

                product.Supplier = supplier;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();
        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

navigationProperty 매개 변수는 설정할 관계를 지정합니다. 엔터티에 탐색 속성이 두 개 이상 있는 경우 문을 더 case 추가할 수 있습니다.

link 매개 변수에는 공급자의 URI가 포함됩니다. Web API는 요청 본문을 자동으로 구문 분석하여 이 매개 변수의 값을 가져옵니다.

공급자를 조회하려면 링크 매개 변수의 일부인 ID(또는 키)가 필요합니다. 이렇게 하려면 다음 도우미 메서드를 사용합니다.

using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using System.Web.OData.Extensions;
using System.Web.OData.Routing;

namespace ProductService
{
    public static class Helpers
    {
        public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

            string serviceRoot = urlHelper.CreateODataLink(
                request.ODataProperties().RouteName, 
                request.ODataProperties().PathHandler, new List<ODataPathSegment>());
            var odataPath = request.ODataProperties().PathHandler.Parse(
                request.ODataProperties().Model, 
                serviceRoot, uri.LocalPath);

            var keySegment = odataPath.Segments.OfType<KeyValuePathSegment>().FirstOrDefault();
            if (keySegment == null)
            {
                throw new InvalidOperationException("The link does not contain a key.");
            }

            var value = ODataUriUtils.ConvertFromUriLiteral(keySegment.Value, ODataVersion.V4);
            return (TKey)value;
        }

    }
}

기본적으로 이 메서드는 OData 라이브러리를 사용하여 URI 경로를 세그먼트로 분할하고, 키가 포함된 세그먼트를 찾고, 키를 올바른 형식으로 변환합니다.

엔터티 간 관계 삭제

관계를 삭제하기 위해 클라이언트는 HTTP DELETE 요청을 $ref URI로 보냅니다.

DELETE http://host/Products(1)/Supplier/$ref

Product와 Supplier 간의 관계를 삭제하는 컨트롤러 메서드는 다음과 같습니다.

public class ProductsController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        string navigationProperty, [FromBody] Uri link)
    {
        var product = db.Products.SingleOrDefault(p => p.Id == key);
        if (product == null)
        {
            return NotFound();
        }

        switch (navigationProperty)
        {
            case "Supplier":
                product.Supplier = null;
                break;

            default:
                return StatusCode(HttpStatusCode.NotImplemented);
        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }        

    // Other controller methods not shown.
}

이 경우 는 Product.Supplier 1 대 다 관계의 "1" 끝이므로 를 로 설정 Product.Suppliernull하면 관계를 제거할 수 있습니다.

관계의 "다" 끝에서 클라이언트는 제거할 관련 엔터티를 지정해야 합니다. 이렇게 하려면 클라이언트는 요청의 쿼리 문자열에 관련 엔터티의 URI를 보냅니다. 예를 들어 "Supplier 1"에서 "Product 1"을 제거하려면 다음을 수행합니다.

DELETE http://host/Suppliers(1)/Products/$ref?$id=http://host/Products(1)

Web API에서 이를 지원하려면 메서드에 추가 매개 변수를 DeleteRef 포함해야 합니다. 다음은 관계로부터 Supplier.Products 제품을 제거하는 컨트롤러 방법입니다.

public class SuppliersController : ODataController
{
    public async Task<IHttpActionResult> DeleteRef([FromODataUri] int key, 
        [FromODataUri] string relatedKey, string navigationProperty)
    {
        var supplier = await db.Suppliers.SingleOrDefaultAsync(p => p.Id == key);
        if (supplier == null)
        {
            return StatusCode(HttpStatusCode.NotFound);
        }

        switch (navigationProperty)
        {
            case "Products":
                var productId = Convert.ToInt32(relatedKey);
                var product = await db.Products.SingleOrDefaultAsync(p => p.Id == productId);

                if (product == null)
                {
                    return NotFound();
                }
                product.Supplier = null;
                break;
            default:
                return StatusCode(HttpStatusCode.NotImplemented);

        }
        await db.SaveChangesAsync();

        return StatusCode(HttpStatusCode.NoContent);
    }

    // Other controller methods not shown.
}

매개 변수는 공급자의 키이며 relatedKey 매개 변수는 제품에서 Products 관계에서 제거할 키입니다. Web API는 쿼리 문자열에서 키를 자동으로 가져옵니다.