ASP.NET Web API 2의 특성 라우팅

라우팅 은 Web API가 URI를 작업에 일치시키는 방법입니다. Web API 2는 특성 라우팅이라는 새로운 유형의 라우팅을 지원합니다. 이름에서 알 수 있듯이 특성 라우팅은 특성을 사용하여 경로를 정의합니다. 특성 라우팅을 사용하면 웹 API의 URI를 더 잘 제어할 수 있습니다. 예를 들어 리소스의 계층 구조를 설명하는 URI를 쉽게 만들 수 있습니다.

규칙 기반 라우팅이라고 하는 이전 라우팅 스타일은 여전히 완전히 지원됩니다. 실제로 동일한 프로젝트에서 두 기술을 결합할 수 있습니다.

이 항목에서는 특성 라우팅을 사용하도록 설정하는 방법을 보여 주고 특성 라우팅에 대한 다양한 옵션을 설명합니다. 특성 라우팅을 사용하는 엔드 투 엔드 자습서는 Web API 2에서 특성 라우팅을 사용하여 REST API 만들기를 참조하세요.

사전 요구 사항

Visual Studio 2017 Community, Professional 또는 Enterprise Edition

또는 NuGet 패키지 관리자를 사용하여 필요한 패키지를 설치합니다. Visual Studio의 도구 메뉴에서 NuGet 패키지 관리자를 선택한 다음 패키지 관리자 콘솔을 선택합니다. 패키지 관리자 콘솔 창에서 다음 명령을 입력합니다.

Install-Package Microsoft.AspNet.WebApi.WebHost

특성 라우팅을 사용하는 이유

Web API의 첫 번째 릴리스에서는 규칙 기반 라우팅을 사용했습니다. 이러한 유형의 라우팅에서는 기본적으로 매개 변수가 있는 문자열인 하나 이상의 경로 템플릿을 정의합니다. 프레임워크는 요청을 받으면 경로 템플릿에 대한 URI와 일치합니다. 규칙 기반 라우팅에 대한 자세한 내용은 ASP.NET Web API 라우팅을 참조하세요.

규칙 기반 라우팅의 한 가지 장점은 템플릿이 단일 위치에 정의되고 라우팅 규칙이 모든 컨트롤러에 일관되게 적용된다는 것입니다. 아쉽게도 규칙 기반 라우팅은 RESTful API에서 일반적인 특정 URI 패턴을 지원하기 어렵게 만듭니다. 예를 들어 리소스에는 종종 자식 리소스가 포함됩니다. 고객은 주문이 있고, 영화에는 행위자가 있고, 책에는 저자가 있습니다. 이러한 관계를 반영하는 URI를 만드는 것은 자연스러운 일입니다.

/customers/1/orders

이러한 유형의 URI는 규칙 기반 라우팅을 사용하여 만들기 어렵습니다. 수행할 수 있지만 컨트롤러 또는 리소스 종류가 많은 경우 결과가 잘 조정되지 않습니다.

특성 라우팅을 사용하면 이 URI에 대한 경로를 정의하는 것이 간단합니다. 컨트롤러 작업에 특성을 추가하기만 하면 됩니다.

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

다음은 특성 라우팅을 쉽게 만드는 몇 가지 다른 패턴입니다.

API 버전 관리

이 예제에서는 "/api/v1/products"가 "/api/v2/products"가 아닌 다른 컨트롤러로 라우팅됩니다.

/api/v1/products /api/v2/products

오버로드된 URI 세그먼트

이 예제에서 "1"은 주문 번호이지만 "보류 중"은 컬렉션에 매핑됩니다.

/orders/1 /orders/pending

여러 매개 변수 형식

이 예제에서 "1"은 주문 번호이지만 "2013/06/16"은 날짜를 지정합니다.

/orders/1 /orders/2013/06/16

특성 라우팅 사용

특성 라우팅을 사용하도록 설정하려면 구성 중에 MapHttpAttributeRoutes를 호출합니다 . 이 확장 메서드는 System.Web.Http.HttpConfigurationExtensions 클래스에 정의되어 있습니다.

using System.Web.Http;

namespace WebApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            // Other Web API configuration not shown.
        }
    }
}

특성 라우팅은 규칙 기반 라우팅과 결합할 수 있습니다. 규칙 기반 경로를 정의하려면 MapHttpRoute 메서드를 호출합니다.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Attribute routing.
        config.MapHttpAttributeRoutes();

        // Convention-based routing.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

Web API 구성에 대한 자세한 내용은 구성 ASP.NET Web API 2를 참조하세요.

참고: Web API 1에서 마이그레이션

Web API 2 이전의 Web API 프로젝트 템플릿은 다음과 같이 코드를 생성했습니다.

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

특성 라우팅을 사용하도록 설정하면 이 코드는 예외를 throw합니다. 특성 라우팅을 사용하도록 기존 Web API 프로젝트를 업그레이드하는 경우 이 구성 코드를 다음으로 업데이트해야 합니다.

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

참고

자세한 내용은 ASP.NET 호스팅을 사용하여 Web API 구성을 참조하세요.

경로 특성 추가

특성을 사용하여 정의된 경로의 예는 다음과 같습니다.

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

"customers/{customerId}/orders" 문자열은 경로에 대한 URI 템플릿입니다. Web API는 요청 URI를 템플릿과 일치시키려고 시도합니다. 이 예제에서 "customers" 및 "orders"는 리터럴 세그먼트이고 "{customerId}"는 변수 매개 변수입니다. 다음 URI는 이 템플릿과 일치합니다.

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

이 항목의 뒷부분에 설명된 제약 조건을 사용하여 일치를 제한할 수 있습니다.

경로 템플릿의 "{customerId}" 매개 변수는 메서드의 customerId 매개 변수 이름과 일치합니다. Web API가 컨트롤러 작업을 호출하면 경로 매개 변수를 바인딩하려고 시도합니다. 예를 들어 URI가 인 경우 Web API는 http://example.com/customers/1/orders작업의 customerId 매개 변수에 "1" 값을 바인딩하려고 합니다.

URI 템플릿에는 다음과 같은 여러 매개 변수가 있을 수 있습니다.

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

경로 특성이 없는 컨트롤러 메서드는 규칙 기반 라우팅을 사용합니다. 이렇게 하면 동일한 프로젝트에서 두 가지 유형의 라우팅을 결합할 수 있습니다.

HTTP 메서드

또한 Web API는 요청의 HTTP 메서드(GET, POST 등)를 기반으로 작업을 선택합니다. 기본적으로 Web API는 컨트롤러 메서드 이름의 시작과 대/소문자를 구분하지 않는 일치 항목을 찾습니다. 예를 들어 라는 PutCustomers 컨트롤러 메서드는 HTTP PUT 요청과 일치합니다.

다음 특성으로 메서드를 데코레이팅하여 이 규칙을 재정의할 수 있습니다.

  • [HttpDelete]
  • [HttpGet]
  • [HttpHead]
  • [HttpOptions]
  • [HttpPatch]
  • [HttpPost]
  • [HttpPut]

다음 예제에서 Web API는 CreateBook 메서드를 HTTP POST 요청에 매핑합니다.

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

비표준 메서드를 비롯한 다른 모든 HTTP 메서드의 경우 HTTP 메서드 목록을 사용하는 AcceptVerbs 특성을 사용합니다.

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

경로 접두사

컨트롤러의 경로는 모두 동일한 접두사로 시작하는 경우가 많습니다. 예:

public class BooksController : ApiController
{
    [Route("api/books")]
    public IEnumerable<Book> GetBooks() { ... }

    [Route("api/books/{id:int}")]
    public Book GetBook(int id) { ... }

    [Route("api/books")]
    [HttpPost]
    public HttpResponseMessage CreateBook(Book book) { ... }
}

[RoutePrefix] 특성을 사용하여 전체 컨트롤러에 대한 공통 접두사를 설정할 수 있습니다.

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET api/books
    [Route("")]
    public IEnumerable<Book> Get() { ... }

    // GET api/books/5
    [Route("{id:int}")]
    public Book Get(int id) { ... }

    // POST api/books
    [Route("")]
    public HttpResponseMessage Post(Book book) { ... }
}

메서드 특성에서 타일드(~)를 사용하여 경로 접두사를 재정의합니다.

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}

경로 접두사에는 매개 변수가 포함될 수 있습니다.

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

경로 제약 조건

경로 제약 조건을 사용하면 경로 템플릿의 매개 변수가 일치하는 방식을 제한할 수 있습니다. 일반적인 구문은 "{parameter:constraint}"입니다. 예:

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

여기서 첫 번째 경로는 URI의 "id" 세그먼트가 정수인 경우에만 선택됩니다. 그렇지 않으면 두 번째 경로가 선택됩니다.

다음 표에서는 지원되는 제약 조건을 나열합니다.

제약 조건 설명 예제
alpha 대문자 또는 소문자 라틴어 알파벳 문자(a-z, A-Z)와 일치합니다. {x:alpha}
bool 부울 값과 일치합니다. {x:bool}
Datetime DateTime 값과 일치합니다. {x:datetime}
decimal 10진수 값과 일치합니다. {x:decimal}
double 64비트 부동 소수점 값과 일치합니다. {x:double}
float 32비트 부동 소수점 값과 일치합니다. {x:float}
guid GUID 값과 일치합니다. {x:guid}
int 32비트 정수 값과 일치합니다. {x:int}
length 지정된 길이 또는 지정된 길이 범위 내의 문자열과 일치합니다. {x:length(6)} {x:length(1,20)}
long 64비트 정수 값과 일치합니다. {x:long}
max 최대값을 가진 정수와 일치합니다. {x:max(10)}
Maxlength 최대 길이를 가진 문자열과 일치합니다. {x:maxlength(10)}
최솟값이 있는 정수와 일치합니다. {x:min(10)}
minlength 최소 길이로 문자열을 찾습니다. {x:minlength(10)}
range 값 범위 내의 정수와 일치합니다. {x:range(10,50)}
regex 정규식과 일치합니다. {x:regex(^\d{3}-\d{3}-\d{4}$)}

"min"과 같은 일부 제약 조건은 인수를 괄호로 사용합니다. 콜론으로 구분된 매개 변수에 여러 제약 조건을 적용할 수 있습니다.

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

사용자 지정 경로 제약 조건

IHttpRouteConstraint 인터페이스를 구현하여 사용자 지정 경로 제약 조건을 만들 수 있습니다. 예를 들어 다음 제약 조건은 매개 변수를 0이 아닌 정수 값으로 제한합니다.

public class NonZeroConstraint : IHttpRouteConstraint
{
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, 
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            long longValue;
            if (value is long)
            {
                longValue = (long)value;
                return longValue != 0;
            }

            string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (Int64.TryParse(valueString, NumberStyles.Integer, 
                CultureInfo.InvariantCulture, out longValue))
            {
                return longValue != 0;
            }
        }
        return false;
    }
}

다음 코드는 제약 조건을 등록하는 방법을 보여줍니다.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

이제 경로에 제약 조건을 적용할 수 있습니다.

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

IInlineConstraintResolver 인터페이스를 구현하여 전체 DefaultInlineConstraintResolver 클래스를 바꿀 수도 있습니다. 이렇게 하면 IInlineConstraintResolver 구현에서 특별히 추가하지 않는 한 모든 기본 제공 제약 조건이 대체됩니다.

선택적 URI 매개 변수 및 기본값

경로 매개 변수에 물음표를 추가하여 URI 매개 변수를 선택적으로 만들 수 있습니다. 경로 매개 변수가 선택 사항인 경우 메서드 매개 변수에 대한 기본값을 정의해야 합니다.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

이 예제에서 및 /api/books/locale/api/books/locale/1033 동일한 리소스를 반환합니다.

또는 다음과 같이 경로 템플릿 내에서 기본값을 지정할 수 있습니다.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

이는 이전 예제와 거의 동일하지만 기본값이 적용될 때 약간의 동작 차이가 있습니다.

  • 첫 번째 예제("{lcid:int?}")에서 기본값 1033은 메서드 매개 변수에 직접 할당되므로 매개 변수는 이 정확한 값을 갖습니다.
  • 두 번째 예제("{lcid:int=1033}")에서 기본값 "1033"은 모델 바인딩 프로세스를 진행합니다. 기본 model-binder는 "1033"을 숫자 값 1033으로 변환합니다. 그러나 다른 작업을 수행할 수 있는 사용자 지정 모델 바인더를 연결할 수 있습니다.

(대부분의 경우 파이프라인에 사용자 지정 모델 바인더가 없는 경우 두 양식은 동일합니다.)

경로 이름

Web API에서 모든 경로에는 이름이 있습니다. 경로 이름은 HTTP 응답에 링크를 포함할 수 있도록 링크를 생성하는 데 유용합니다.

경로 이름을 지정하려면 특성에서 Name 속성을 설정합니다. 다음 예제에서는 경로 이름을 설정하는 방법과 링크를 생성할 때 경로 이름을 사용하는 방법을 보여줍니다.

public class BooksController : ApiController
{
    [Route("api/books/{id}", Name="GetBookById")]
    public BookDto GetBook(int id) 
    {
        // Implementation not shown...
    }

    [Route("api/books")]
    public HttpResponseMessage Post(Book book)
    {
        // Validate and add book to database (not shown)

        var response = Request.CreateResponse(HttpStatusCode.Created);

        // Generate a link to the new book and set the Location header in the response.
        string uri = Url.Link("GetBookById", new { id = book.BookId });
        response.Headers.Location = new Uri(uri);
        return response;
    }
}

경로 순서

프레임워크가 경로와 URI를 일치시키려고 하면 특정 순서로 경로를 평가합니다. 순서를 지정하려면 경로 특성에서 Order 속성을 설정합니다. 낮은 값이 먼저 평가됩니다. 기본 순서 값은 0입니다.

총 순서를 결정하는 방법은 다음과 같습니다.

  1. 경로 특성의 Order 속성을 비교합니다.

  2. 경로 템플릿에서 각 URI 세그먼트를 확인합니다. 각 세그먼트에 대해 다음과 같이 순서를 지정합니다.

    1. 리터럴 세그먼트.
    2. 제약 조건을 사용하여 매개 변수를 라우팅합니다.
    3. 제약 조건 없이 매개 변수를 라우팅합니다.
    4. 제약 조건이 있는 와일드카드 매개 변수 세그먼트입니다.
    5. 제약 조건이 없는 와일드카드 매개 변수 세그먼트입니다.
  3. 동률의 경우 경로 템플릿의 대/소문자를 구분하지 않는 서수 문자열 비교(OrdinalIgnoreCase)를 기준으로 경로가 정렬됩니다.

다음은 예제입니다. 다음 컨트롤러를 정의한다고 가정해 보겠습니다.

[RoutePrefix("orders")]
public class OrdersController : ApiController
{
    [Route("{id:int}")] // constrained parameter
    public HttpResponseMessage Get(int id) { ... }

    [Route("details")]  // literal
    public HttpResponseMessage GetDetails() { ... }

    [Route("pending", RouteOrder = 1)]
    public HttpResponseMessage GetPending() { ... }

    [Route("{customerName}")]  // unconstrained parameter
    public HttpResponseMessage GetByCustomer(string customerName) { ... }

    [Route("{*date:datetime}")]  // wildcard
    public HttpResponseMessage Get(DateTime date) { ... }
}

이러한 경로는 다음과 같이 정렬됩니다.

  1. orders/details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. orders/pending

"details"는 리터럴 세그먼트이며 "{id}" 앞에 표시되지만 Order 속성이 1이므로 "보류 중"이 마지막으로 표시됩니다. (이 예제에서는 "details" 또는 "pending"이라는 고객이 없다고 가정합니다. 일반적으로 모호한 경로를 피하려고 합니다. 이 예제에서 에 대한 GetByCustomer 더 나은 경로 템플릿은 "customers/{customerName}"입니다.