ASP.NET Web API 매개 변수 바인딩

ASP.NET Core 웹 API를 사용하는 것이 좋습니다. ASP.NET 4.x Web API보다 다음과 같은 이점이 있습니다.

  • ASP.NET Core Windows, macOS 및 Linux에서 최신 클라우드 기반 웹앱을 빌드하기 위한 오픈 소스 플랫폼 간 프레임워크입니다.
  • ASP.NET Core MVC 컨트롤러 및 웹 API 컨트롤러가 통합됩니다.
  • 테스트 가능성을 고려하여 설계되었습니다.
  • Windows, macOS 및 Linux에서 개발하고 실행할 수 있습니다.
  • 오픈 소스이며 커뮤니티에 중점을 둡니다.
  • 최신 클라이언트 쪽 프레임워크 및 개발 워크플로의 통합.
  • 클라우드 지원 환경 기반 구성 시스템입니다.
  • 기본 제공 종속성 주입.
  • 간단한 고성능 모듈식 HTTP 요청 파이프라인을 포함합니다.
  • Kestrel, IIS, HTTP.sys, Nginx, ApacheDocker에서 호스트하는 기능.
  • Side-by-side 버전 관리.
  • 최신 웹 개발을 간소화하는 도구를 포함합니다.

이 문서에서는 Web API가 매개 변수를 바인딩하는 방법과 바인딩 프로세스를 사용자 지정하는 방법을 설명합니다. Web API는 컨트롤러에서 메서드를 호출할 때 바인딩이라는 프로세스인 매개 변수에 대한 값을 설정해야 합니다.

기본적으로 Web API는 다음 규칙을 사용하여 매개 변수를 바인딩합니다.

  • 매개 변수가 "단순" 형식인 경우 Web API는 URI에서 값을 가져옵니다. 단순 형식에는 .NET 기본 형식 (int, bool, double 등)과 TimeSpan, DateTime, Guid, 10진수문자열과 문자열에서 변환할 수 있는 형식 변환기가 있는 모든 형식 이 포함됩니다. (나중에 형식 변환기를 참조하세요.)
  • 복잡한 형식의 경우 Web API는 미디어 형식 포맷터를 사용하여 메시지 본문에서 값을 읽으려고 합니다.

예를 들어 일반적인 Web API 컨트롤러 메서드는 다음과 같습니다.

HttpResponseMessage Put(int id, Product item) { ... }

id 매개 변수는 "단순" 형식이므로 Web API는 요청 URI에서 값을 가져오려고 시도합니다. 항목 매개 변수는 복합 형식이므로 Web API는 미디어 형식 포맷터를 사용하여 요청 본문에서 값을 읽습니다.

URI에서 값을 가져오기 위해 Web API는 경로 데이터 및 URI 쿼리 문자열을 찾습니다. 라우팅 시스템이 URI를 구문 분석하고 경로와 일치할 때 경로 데이터가 채워집니다. 자세한 내용은 라우팅 및 작업 선택을 참조하세요.

이 문서의 나머지 부분에는 모델 바인딩 프로세스를 사용자 지정하는 방법을 보여 줍니다. 그러나 복합 형식의 경우 가능하면 미디어 형식 포맷터를 사용하는 것이 좋습니다. HTTP의 주요 원칙은 콘텐츠 협상을 사용하여 리소스의 표현을 지정하여 메시지 본문에 리소스를 보내는 것입니다. 미디어 형식 포맷터는 정확히 이러한 용도로 설계되었습니다.

[FromUri] 사용

Web API가 URI에서 복합 형식을 읽도록 하려면 매개 변수에 [FromUri] 특성을 추가합니다. 다음 예제에서는 URI에서 를 GeoPoint 가져오는 GeoPoint 컨트롤러 메서드와 함께 형식을 정의합니다.

public class GeoPoint
{
    public double Latitude { get; set; } 
    public double Longitude { get; set; }
}

public ValuesController : ApiController
{
    public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}

클라이언트는 위도 및 경도 값을 쿼리 문자열에 넣을 수 있으며 Web API는 이를 사용하여 를 생성 GeoPoint합니다. 예:

http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989

[FromBody] 사용

Web API가 요청 본문에서 단순 형식을 읽도록 하려면 매개 변수에 [FromBody] 특성을 추가합니다.

public HttpResponseMessage Post([FromBody] string name) { ... }

이 예제에서 Web API는 미디어 형식 포맷터를 사용하여 요청 본문에서 이름 값을 읽습니다. 다음은 클라이언트 요청 예제입니다.

POST http://localhost:5076/api/values HTTP/1.1
User-Agent: Fiddler
Host: localhost:5076
Content-Type: application/json
Content-Length: 7

"Alice"

매개 변수에 [FromBody]가 있는 경우 Web API는 Content-Type 헤더를 사용하여 포맷터를 선택합니다. 이 예제에서 콘텐츠 형식은 "application/json"이고 요청 본문은 원시 JSON 문자열(JSON 개체가 아님)입니다.

메시지 본문에서 최대 하나의 매개 변수를 읽을 수 있습니다. 이렇게 하면 작동하지 않습니다.

// Caution: Will not work!    
public HttpResponseMessage Post([FromBody] int id, [FromBody] string name) { ... }

이 규칙의 이유는 요청 본문이 한 번만 읽을 수 있는 버퍼링되지 않은 스트림에 저장될 수 있기 때문입니다.

형식 변환기

TypeConverter를 만들고 문자열 변환을 제공하여 Web API가 클래스를 간단한 형식으로 처리하도록 만들 수 있습니다(Web API가 URI에서 바인딩하려고 시도하도록).

다음 코드는 지리적 지점을 나타내는 클래스와 문자열에서 인스턴스로 변환하는 TypeConverterGeoPoint 보여 GeoPoint 줍니다. 클래스는 GeoPoint 형식 변환기를 지정하기 위해 [TypeConverter] 특성으로 데코레이트됩니다. (이 예제는 MVC/WebAPI의 작업 서명에서 사용자 지정 개체에 바인딩하는 방법 Mike Stall의 블로그 게시물에서 영감을 얻었습니다.)

[TypeConverter(typeof(GeoPointConverter))]
public class GeoPoint
{
    public double Latitude { get; set; } 
    public double Longitude { get; set; }

    public static bool TryParse(string s, out GeoPoint result)
    {
        result = null;

        var parts = s.Split(',');
        if (parts.Length != 2)
        {
            return false;
        }

        double latitude, longitude;
        if (double.TryParse(parts[0], out latitude) &&
            double.TryParse(parts[1], out longitude))
        {
            result = new GeoPoint() { Longitude = longitude, Latitude = latitude };
            return true;
        }
        return false;
    }
}

class GeoPointConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }
        return base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, 
        CultureInfo culture, object value)
    {
        if (value is string)
        {
            GeoPoint point;
            if (GeoPoint.TryParse((string)value, out point))
            {
                return point;
            }
        }
        return base.ConvertFrom(context, culture, value);
    }
}

이제 Web API는 간단한 형식으로 처리 GeoPoint 됩니다. 즉, URI에서 매개 변수를 바인딩 GeoPoint 하려고 합니다. 매개 변수에 [FromUri]를 포함할 필요가 없습니다.

public HttpResponseMessage Get(GeoPoint location) { ... }

클라이언트는 다음과 같은 URI를 사용하여 메서드를 호출할 수 있습니다.

http://localhost/api/values/?location=47.678558,-122.130989

모델 바인더

형식 변환기보다 더 유연한 옵션은 사용자 지정 모델 바인더를 만드는 것입니다. 모델 바인더를 사용하면 HTTP 요청, 작업 설명 및 경로 데이터의 원시 값과 같은 항목에 액세스할 수 있습니다.

모델 바인더를 만들려면 IModelBinder 인터페이스를 구현합니다. 이 인터페이스는 단일 메서드 BindModel을 정의합니다.

bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext);

다음은 개체에 대한 GeoPoint 모델 바인더입니다.

public class GeoPointModelBinder : IModelBinder
{
    // List of known locations.
    private static ConcurrentDictionary<string, GeoPoint> _locations
        = new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);

    static GeoPointModelBinder()
    {
        _locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
        _locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
        _locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
    }

    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(GeoPoint))
        {
            return false;
        }

        ValueProviderResult val = bindingContext.ValueProvider.GetValue(
            bindingContext.ModelName);
        if (val == null)
        {
            return false;
        }

        string key = val.RawValue as string;
        if (key == null)
        {
            bindingContext.ModelState.AddModelError(
                bindingContext.ModelName, "Wrong value type");
            return false;
        }

        GeoPoint result;
        if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
        {
            bindingContext.Model = result;
            return true;
        }

        bindingContext.ModelState.AddModelError(
            bindingContext.ModelName, "Cannot convert value to GeoPoint");
        return false;
    }
}

모델 바인더는 값 공급자로부터 원시 입력 값을 가져옵니다. 이 디자인은 두 가지 고유한 함수를 구분합니다.

  • 값 공급자는 HTTP 요청을 받아 키-값 쌍의 사전을 채웁니다.
  • 모델 바인더는 이 사전을 사용하여 모델을 채웁다.

Web API의 기본값 공급자는 경로 데이터 및 쿼리 문자열에서 값을 가져옵니다. 예를 들어 URI가 인 경우 값 공급자는 http://localhost/api/values/1?location=48,-122다음 키-값 쌍을 만듭니다.

  • id = "1"
  • location = "48,-122"

(기본 경로 템플릿인 "api/{controller}/{id}"라고 가정합니다.)

바인딩할 매개 변수의 이름은 ModelBindingContext.ModelName 속성에 저장됩니다. 모델 바인더는 사전에서 이 값이 있는 키를 찾습니다. 값이 존재하고 로 GeoPoint변환할 수 있는 경우 모델 바인더는 바인딩된 값을 ModelBindingContext.Model 속성에 할당합니다.

모델 바인더는 단순 형식 변환으로 제한되지 않습니다. 이 예제에서 모델 바인더는 먼저 알려진 위치의 테이블을 살펴보고, 실패하면 형식 변환을 사용합니다.

모델 바인더 설정

모델 바인더를 설정하는 방법에는 여러 가지가 있습니다. 먼저 매개 변수에 [ModelBinder] 특성을 추가할 수 있습니다.

public HttpResponseMessage Get([ModelBinder(typeof(GeoPointModelBinder))] GeoPoint location)

형식에 [ModelBinder] 특성을 추가할 수도 있습니다. Web API는 해당 형식의 모든 매개 변수에 대해 지정된 모델 바인더를 사용합니다.

[ModelBinder(typeof(GeoPointModelBinder))]
public class GeoPoint
{
    // ....
}

마지막으로 HttpConfiguration에 모델 바인더 공급자를 추가할 수 있습니다. 모델 바인더 공급자는 단순히 모델 바인더를 만드는 팩터리 클래스입니다. ModelBinderProvider 클래스에서 파생하여 공급자를 만들 수 있습니다. 그러나 모델 바인더가 단일 형식을 처리하는 경우 이 용도로 설계된 기본 제공 SimpleModelBinderProvider를 사용하는 것이 더 쉽습니다. 다음 코드에서는 이 작업을 수행하는 방법을 보여 줍니다.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var provider = new SimpleModelBinderProvider(
            typeof(GeoPoint), new GeoPointModelBinder());
        config.Services.Insert(typeof(ModelBinderProvider), 0, provider);

        // ...
    }
}

모델 바인딩 공급자를 사용하면 미디어 형식 포맷터가 아닌 모델 바인더를 사용해야 함을 Web API에 알리기 위해 매개 변수에 [ModelBinder] 특성을 추가해야 합니다. 하지만 이제 특성에서 모델 바인더 유형을 지정할 필요가 없습니다.

public HttpResponseMessage Get([ModelBinder] GeoPoint location) { ... }

값 공급자

모델 바인더가 값 공급자로부터 값을 가져오는 것을 언급했습니다. 사용자 지정 값 공급자를 작성하려면 IValueProvider 인터페이스를 구현합니다. 다음은 요청의 쿠키에서 값을 가져오는 예제입니다.

public class CookieValueProvider : IValueProvider
{
    private Dictionary<string, string> _values;

    public CookieValueProvider(HttpActionContext actionContext)
    {
        if (actionContext == null)
        {
            throw new ArgumentNullException("actionContext");
        }

        _values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var cookie in actionContext.Request.Headers.GetCookies())
        {
            foreach (CookieState state in cookie.Cookies)
            {
                _values[state.Name] = state.Value;
            }
        }
    }

    public bool ContainsPrefix(string prefix)
    {
        return _values.Keys.Contains(prefix);
    }

    public ValueProviderResult GetValue(string key)
    {
        string value;
        if (_values.TryGetValue(key, out value))
        {
            return new ValueProviderResult(value, value, CultureInfo.InvariantCulture);
        }
        return null;
    }
}

ValueProviderFactory 클래스에서 파생하여 값 공급자 팩터리를 만들어야 합니다.

public class CookieValueProviderFactory : ValueProviderFactory
{
    public override IValueProvider GetValueProvider(HttpActionContext actionContext)
    {
        return new CookieValueProvider(actionContext);
    }
}

다음과 같이 값 공급자 팩터리를 HttpConfiguration 에 추가합니다.

public static void Register(HttpConfiguration config)
{
    config.Services.Add(typeof(ValueProviderFactory), new CookieValueProviderFactory());

    // ...
}

Web API는 모든 값 공급자를 구성하므로 모델 바인더가 ValueProvider.GetValue를 호출하면 모델 바인더는 이를 생성할 수 있는 첫 번째 값 공급자로부터 값을 받습니다.

또는 다음과 같이 ValueProvider 특성을 사용하여 매개 변수 수준에서 값 공급자 팩터리를 설정할 수 있습니다.

public HttpResponseMessage Get(
    [ValueProvider(typeof(CookieValueProviderFactory))] GeoPoint location)

그러면 Web API가 지정된 값 공급자 팩터리와 모델 바인딩을 사용하고 다른 등록된 값 공급자를 사용하지 않도록 지시합니다.

HttpParameterBinding

모델 바인더는 보다 일반적인 메커니즘의 특정 instance. [ModelBinder] 특성을 보면 추상 ParameterBindingAttribute 클래스에서 파생되는 것을 볼 수 있습니다. 이 클래스는 HttpParameterBinding 개체를 반환하는 단일 메서드 GetBinding을 정의합니다.

public abstract class ParameterBindingAttribute : Attribute
{
    public abstract HttpParameterBinding GetBinding(HttpParameterDescriptor parameter);
}

HttpParameterBinding은 매개 변수를 값에 바인딩합니다. [ModelBinder]의 경우 특성은 IModelBinder를 사용하여 실제 바인딩을 수행하는 HttpParameterBinding 구현을 반환합니다. 사용자 고유 의 HttpParameterBinding을 구현할 수도 있습니다.

예를 들어 요청의 및 if-none-match 헤더에서 if-match ETag를 가져올 것이라고 가정합니다. 먼저 ETag를 나타내는 클래스를 정의합니다.

public class ETag
{
    public string Tag { get; set; }
}

헤더 또는 if-none-match 헤더에서 if-match ETag를 가져올지 여부를 나타내는 열거형도 정의합니다.

public enum ETagMatch
{
    IfMatch,
    IfNoneMatch
}

다음은 원하는 헤더에서 ETag를 가져오고 ETag 형식의 매개 변수에 바인딩하는 HttpParameterBinding 입니다.

public class ETagParameterBinding : HttpParameterBinding
{
    ETagMatch _match;

    public ETagParameterBinding(HttpParameterDescriptor parameter, ETagMatch match) 
        : base(parameter)
    {
        _match = match;
    }

    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, 
        HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        EntityTagHeaderValue etagHeader = null;
        switch (_match)
        {
            case ETagMatch.IfNoneMatch:
                etagHeader = actionContext.Request.Headers.IfNoneMatch.FirstOrDefault();
                break;

            case ETagMatch.IfMatch:
                etagHeader = actionContext.Request.Headers.IfMatch.FirstOrDefault();
                break;
        }

        ETag etag = null;
        if (etagHeader != null)
        {
            etag = new ETag { Tag = etagHeader.Tag };
        }
        actionContext.ActionArguments[Descriptor.ParameterName] = etag;

        var tsc = new TaskCompletionSource<object>();
        tsc.SetResult(null);
        return tsc.Task;
    }
}

ExecuteBindingAsync 메서드는 바인딩을 수행합니다. 이 메서드 내에서 바인딩된 매개 변수 값을 HttpActionContextActionArgument 사전에 추가합니다.

참고

ExecuteBindingAsync 메서드가 요청 메시지의 본문을 읽는 경우 WillReadBody 속성을 재정의하여 true를 반환합니다. 요청 본문은 한 번만 읽을 수 있는 버퍼되지 않은 스트림일 수 있으므로 Web API는 최대 하나의 바인딩이 메시지 본문을 읽을 수 있는 규칙을 적용합니다.

사용자 지정 HttpParameterBinding을 적용하려면 ParameterBindingAttribute에서 파생되는 특성을 정의할 수 있습니다. 의 경우 ETagParameterBinding헤더와 헤더에 대해 if-match 하나씩 if-none-match 두 가지 특성을 정의합니다. 둘 다 추상 기본 클래스에서 파생됩니다.

public abstract class ETagMatchAttribute : ParameterBindingAttribute
{
    private ETagMatch _match;

    public ETagMatchAttribute(ETagMatch match)
    {
        _match = match;
    }

    public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
    {
        if (parameter.ParameterType == typeof(ETag))
        {
            return new ETagParameterBinding(parameter, _match);
        }
        return parameter.BindAsError("Wrong parameter type");
    }
}

public class IfMatchAttribute : ETagMatchAttribute
{
    public IfMatchAttribute()
        : base(ETagMatch.IfMatch)
    {
    }
}

public class IfNoneMatchAttribute : ETagMatchAttribute
{
    public IfNoneMatchAttribute()
        : base(ETagMatch.IfNoneMatch)
    {
    }
}

다음은 특성을 사용하는 컨트롤러 메서드입니다 [IfNoneMatch] .

public HttpResponseMessage Get([IfNoneMatch] ETag etag) { ... }

ParameterBindingAttribute 외에도 사용자 지정 HttpParameterBinding을 추가하기 위한 또 다른 후크가 있습니다. HttpConfiguration 개체에서 ParameterBindingRules 속성은 형식의 익명 함수 컬렉션입니다(HttpParameterDescriptor ->HttpParameterBinding). 예를 들어 GET 메서드의 ETag 매개 변수가 와 함께 if-none-match사용하는 규칙을 추가할 수 있습니다ETagParameterBinding.

config.ParameterBindingRules.Add(p =>
{
    if (p.ParameterType == typeof(ETag) && 
        p.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get))
    {
        return new ETagParameterBinding(p, ETagMatch.IfNoneMatch);
    }
    else
    {
        return null;
    }
});

함수는 바인딩을 적용할 수 없는 매개 변수에 대해 를 반환 null 해야 합니다.

IActionValueBinder

전체 매개 변수 바인딩 프로세스는 플러그형 서비스인 IActionValueBinder에 의해 제어됩니다. IActionValueBinder의 기본 구현은 다음을 수행합니다.

  1. 매개 변수에서 ParameterBindingAttribute를 찾습니다. 여기에는 [FromBody], [FromUri], [ModelBinder] 또는 사용자 지정 특성이 포함됩니다.

  2. 그렇지 않으면 Null이 아닌 HttpParameterBinding을 반환하는 함수에 대해 HttpConfiguration.ParameterBindingRules를 확인합니다.

  3. 그렇지 않으면 앞에서 설명한 기본 규칙을 사용합니다.

    • 매개 변수 형식이 "simple"이거나 형식 변환기가 있는 경우 URI에서 바인딩합니다. 이는 매개 변수에 [FromUri] 특성을 배치하는 것과 같습니다.
    • 그렇지 않으면 메시지 본문에서 매개 변수를 읽으려고 합니다. 이는 매개 변수에 [FromBody] 를 배치하는 것과 같습니다.

원하는 경우 전체 IActionValueBinder 서비스를 사용자 지정 구현으로 바꿀 수 있습니다.

추가 리소스

사용자 지정 매개 변수 바인딩 샘플

Mike Stall은 Web API 매개 변수 바인딩에 대한 일련의 블로그 게시물을 작성했습니다.