ASP.NET Web API 2.2를 사용 하는 OData v4의 엔터티 관계Entity Relations in OData v4 Using ASP.NET Web API 2.2

Mike Wassonby Mike Wasson

대부분의 데이터 집합은 엔터티 간의 관계를 정의 합니다. 고객에 게는 주문이 있습니다. 서적에는 작성자가 있습니다. 제품에는 공급자가 있습니다.Most data sets define relations between entities: Customers have orders; books have authors; products have suppliers. 클라이언트는 OData를 사용 하 여 엔터티 관계를 탐색할 수 있습니다.Using OData, clients can navigate over entity relations. 제품을 제공 하는 경우 공급자를 찾을 수 있습니다.Given a product, you can find the supplier. 관계를 만들거나 제거할 수도 있습니다.You can also create or remove relationships. 예를 들어 제품의 공급 업체를 설정할 수 있습니다.For example, you can set the supplier for a product.

이 자습서에서는 ASP.NET Web API를 사용 하 여 OData v4에서 이러한 작업을 지 원하는 방법을 보여 줍니다.This tutorial shows how to support these operations in OData v4 using ASP.NET Web API. 자습서에서는 ASP.NET Web API 2를 사용 하 여 OData V4 끝점 만들기자습서를 빌드합니다.The tutorial builds on the tutorial Create an OData v4 Endpoint Using ASP.NET Web API 2.

자습서에서 사용 되는 소프트웨어 버전Software versions used in the tutorial

  • 웹 API 2.1Web API 2.1
  • OData v4OData v4
  • Visual Studio 2013 (Visual Studio 2017 다운로드)Visual Studio 2013 (download Visual Studio 2017 here)
  • Entity Framework 6Entity Framework 6
  • .NET 4.5.NET 4.5

자습서 버전Tutorial versions

OData 버전 3의 경우 odata v3에서 엔터티 관계 지원을 참조 하세요.For the OData Version 3, see Supporting Entity Relations in OData v3.

공급자 엔터티 추가Add a Supplier Entity

Note

자습서에서는 ASP.NET Web API 2를 사용 하 여 OData V4 끝점 만들기자습서를 빌드합니다.The tutorial builds on the tutorial Create an OData v4 Endpoint Using ASP.NET Web API 2.

먼저 관련 엔터티가 필요 합니다.First, we need a related entity. 모델 폴더에 Supplier 라는 클래스를 추가 합니다.Add a class named Supplier in the Models folder.

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 클래스에 탐색 속성을 추가 합니다.Add a navigation property to the Product class:

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 ProductsContext 클래스에 새 Dbset 을 추가 하 여 데이터베이스에 공급자 테이블을 포함 합니다.Add a new DbSet to the ProductsContext class, so that Entity Framework will include the Supplier table in the database.

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" 엔터티 집합을 추가 합니다.In WebApiConfig.cs, add a "Suppliers" entity set to the entity data model:

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 컨트롤러 추가Add a Suppliers Controller

Controllers 폴더에 SuppliersController 클래스를 추가 합니다.Add a SuppliersController class to the Controllers folder.

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 작업을 추가 하는 방법을 표시 하지 않습니다.I won't show how to add CRUD operations for this controller. 이러한 단계는 Products 컨트롤러의 경우와 동일 합니다 ( OData V4 끝점 만들기참조).The steps are the same as for the Products controller (see Create an OData v4 Endpoint).

제품에 대 한 공급자를 가져오기 위해 클라이언트는 GET 요청을 보냅니다.To get the supplier for a product, the client sends a GET request:

GET /Products(1)/Supplier

이 요청을 지원 하려면 ProductsController 클래스에 다음 메서드를 추가 합니다.To support this request, add the following method to the ProductsController class:

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.
}

이 메서드는 기본 명명 규칙을 사용 합니다.This method uses a default naming convention

  • 메서드 이름: GetX, 여기서 X는 탐색 속성입니다.Method name: GetX, where X is the navigation property.
  • 매개 변수 이름: keyParameter name: key

이 명명 규칙을 따르는 경우 Web API는 자동으로 HTTP 요청을 컨트롤러 메서드에 매핑합니다.If you follow this naming convention, Web API automatically maps the HTTP request to the controller method.

예제 HTTP 요청:Example HTTP request:

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

HTTP 응답 예제:Example HTTP response:

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"
}

이전 예제에서 제품에는 하나의 공급 업체가 있습니다.In the previous example, a product has one supplier. 탐색 속성은 컬렉션을 반환할 수도 있습니다.A navigation property can also return a collection. 다음 코드는 공급자에 대 한 제품을 가져옵니다.The following code gets the products for a supplier:

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 을 반환>In this case, the method returns an IQueryable instead of a SingleResult<T>

예제 HTTP 요청:Example HTTP request:

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

HTTP 응답 예제:Example HTTP response:

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
    }
  ]
}

엔터티 간 관계 만들기Creating a Relationship Between Entities

OData는 기존의 두 엔터티 간의 관계를 만들거나 제거 하는 것을 지원 합니다.OData supports creating or removing relationships between two existing entities. OData v4 용어에서 관계는 "참조"입니다.In OData v4 terminology, the relationship is a "reference". (OData v3에서 관계를 링크라고 합니다.(In OData v3, the relationship was called a link. 이 자습서에서는 프로토콜 차이가 중요 하지 않습니다.The protocol differences don't matter for this tutorial.)

참조에는 /Entity/NavigationProperty/$ref형식의 고유한 URI가 있습니다.A reference has its own URI, with the form /Entity/NavigationProperty/$ref. 예를 들어 다음은 제품과 공급자 간의 참조를 처리 하는 URI입니다.For example, here is the URI to address the reference between a product and its supplier:

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

관계를 추가 하기 위해 클라이언트는이 주소에 POST 또는 PUT 요청을 보냅니다.To add a relationship, the client sends a POST or PUT request to this address.

  • 탐색 속성이 단일 엔터티 (예: Product.Supplier) 인 경우에는를 입력 합니다.PUT if the navigation property is a single entity, such as Product.Supplier.
  • 탐색 속성이 컬렉션인 경우 POST (예: Supplier.Products)POST if the navigation property is a collection, such as Supplier.Products.

요청의 본문에는 관계에 있는 다른 엔터티의 URI가 포함 됩니다.The body of the request contains the URI of the other entity in the relation. 예제 요청은 다음과 같습니다.Here is an example request:

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)"}

이 예제에서 클라이언트는 /Products(6)/Supplier/$ref에 PUT 요청을 보냅니다 .이는 ID = 6 인 제품의 Supplier에 대 한 $ref URI입니다.In this example, the client sends a PUT request to /Products(6)/Supplier/$ref, which is the $ref URI for the Supplier of the product with ID = 6. 요청이 성공 하면 서버는 204 (콘텐츠 없음) 응답을 보냅니다.If the request succeeds, the server sends a 204 (No Content) response:

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

Product에 관계를 추가 하는 컨트롤러 메서드는 다음과 같습니다.Here is the controller method to add a relationship to a 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 매개 변수는 설정할 관계를 지정 합니다.The navigationProperty parameter specifies which relationship to set. 엔터티에 대 한 탐색 속성이 두 개 이상 있는 경우 더 많은 case 문을 추가할 수 있습니다.(If there is more than one navigation property on the entity, you can add more case statements.)

Link 매개 변수는 공급자의 URI를 포함 합니다.The link parameter contains the URI of the supplier. Web API는 요청 본문을 자동으로 구문 분석 하 여이 매개 변수의 값을 가져옵니다.Web API automatically parses the request body to get the value for this parameter.

공급자를 조회 하려면 링크 매개 변수의 일부인 ID (또는 키)가 필요 합니다.To look up the supplier, we need the ID (or key), which is part of the link parameter. 이렇게 하려면 다음 도우미 메서드를 사용 합니다.To do this, use the following helper method:

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 경로를 세그먼트로 분할 하 고, 키를 포함 하는 세그먼트를 찾고, 키를 올바른 형식으로 변환 합니다.Basically, this method uses the OData library to split the URI path into segments, find the segment that contains the key, and convert the key into the correct type.

엔터티 간 관계 삭제Deleting a Relationship Between Entities

관계를 삭제 하기 위해 클라이언트는 $ref URI에 HTTP DELETE 요청을 보냅니다.To delete a relationship, the client sends an HTTP DELETE request to the $ref URI:

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

제품 및 공급자 간의 관계를 삭제 하는 컨트롤러 메서드는 다음과 같습니다.Here is the controller method to delete the relationship between a Product and a 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" 끝 이므로 Product.Suppliernull로 설정 하기만 하면 관계를 제거할 수 있습니다.In this case, Product.Supplier is the "1" end of a 1-to-many relation, so you can remove the relationship just by setting Product.Supplier to null.

관계의 여러" 끝 "에서 클라이언트는 제거할 관련 엔터티를 지정 해야 합니다.In the "many" end of a relationship, the client must specify which related entity to remove. 이렇게 하기 위해 클라이언트는 요청의 쿼리 문자열에 관련 엔터티의 URI를 보냅니다.To do so, the client sends the URI of the related entity in the query string of the request. 예를 들어 "공급 업체 1"에서 "제품 1"을 제거 하려면 다음을 수행 합니다.For example, to remove "Product 1" from "Supplier 1":

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

Web API에서이를 지원 하려면 DeleteRef 메서드에 추가 매개 변수를 포함 해야 합니다.To support this in Web API, we need to include an extra parameter in the DeleteRef method. Supplier.Products 관계에서 제품을 제거 하는 컨트롤러 메서드는 다음과 같습니다.Here is the controller method to remove a product from the Supplier.Products relation.

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 관계에서 제거할 제품의 키입니다.The key parameter is the key for the supplier, and the relatedKey parameter is the key for the product to remove from the Products relationship. Web API는 쿼리 문자열에서 키를 자동으로 가져옵니다.Note that Web API automatically gets the key from the query string.