ASP.NET Core의 사용자 지정 모델 바인딩Custom Model Binding in ASP.NET Core

작성자: Steve SmithBy Steve Smith

모델 바인딩을 통해 컨트롤러 작업에서 HTTP 요청이 아닌 모델 형식(메서드 인수로 전달된)을 직접 작업할 수 있습니다.Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. 들어오는 요청 데이터와 애플리케이션 모델 간의 매핑은 모델 바인더를 통해 처리됩니다.Mapping between incoming request data and application models is handled by model binders. 개발자는 사용자 지정 모델 바인더를 구현하여 기본 모델 바인딩 기능을 확장할 수 있습니다(일반적으로 개발자가 고유의 공급자를 작성할 필요는 없음).Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

GitHub에서 샘플 보기 또는 다운로드View or download sample from GitHub

기본 모델 바인더 제한 사항Default model binder limitations

기본 모델 바인더는 대부분의 공용 .NET Core 데이터 형식을 지원하며 대부분의 개발자 요구 사항을 충족해야 합니다.The default model binders support most of the common .NET Core data types and should meet most developers' needs. 요청의 텍스트 기반 입력을 모델 형식에 직접 바인딩할 것으로 예상됩니다.They expect to bind text-based input from the request directly to model types. 바인딩하려면 입력을 변환해야 할 수도 있습니다.You might need to transform the input prior to binding it. 예를 들어 모델 데이터를 조회하는 데 사용할 수 있는 키를 갖고 있는 경우For example, when you have a key that can be used to look up model data. 그 키를 기반으로 사용자 지정 모델 바인더를 사용하여 데이터를 가져올 수 있습니다.You can use a custom model binder to fetch data based on the key.

모델 바인딩 검토Model binding review

모델 바인딩은 작업을 수행하는 형식에 대한 특정 정의를 사용합니다.Model binding uses specific definitions for the types it operates on. 단순 형식은 입력의 단일 문자열에서 변환됩니다.A simple type is converted from a single string in the input. 복합 형식은 여러 입력 값에서 변환됩니다.A complex type is converted from multiple input values. 프레임워크는 TypeConverter의 존재 여부에 따라 차이점을 확인합니다.The framework determines the difference based on the existence of a TypeConverter. 외부 리소스가 필요 없는 간단한 string -> SomeType 매핑이 있는 경우 형식 변환기를 만드는 것이 좋습니다.We recommended you create a type converter if you have a simple string -> SomeType mapping that doesn't require external resources.

개발자 고유의 사용자 지정 모델 바인더를 만들기 전에 기존 모델 바인더가 구현되는 방식을 검토하는 것이 좋습니다.Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. base64로 인코딩된 문자열을 바이트 배열로 변환하는 데 사용할 수 있는 ByteArrayModelBinder를 고려해 보세요.Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. 바이트 배열은 종종 파일 또는 데이터베이스 BLOB 필드로 저장됩니다.The byte arrays are often stored as files or database BLOB fields.

ByteArrayModelBinder 사용Working with the ByteArrayModelBinder

Base64로 인코딩된 문자열은 이진 데이터를 나타내는 데 사용할 수 있습니다.Base64-encoded strings can be used to represent binary data. 예를 들어 다음 이미지를 문자열로 인코딩할 수 있습니다.For example, the following image can be encoded as a string.

dotnet botdotnet bot

인코딩된 문자열의 일부가 다음 그림에 표시되어 있습니다.A small portion of the encoded string is shown in the following image:

인코딩된 dotnet botdotnet bot encoded

샘플의 추가 정보에 제공된 지침에 따라 base64로 인코딩된 문자열을 파일로 변환합니다.Follow the instructions in the sample's README to convert the base64-encoded string into a file.

ASP.NET Core MVC는 base64로 인코드된 문자열을 가져온 후 ByteArrayModelBinder를 사용하여 바이트 배열로 변환할 수 있습니다.ASP.NET Core MVC can take a base64-encoded string and use a ByteArrayModelBinder to convert it into a byte array. IModelBinderProvider를 구현하는 ByteArrayModelBinderProviderbyte[] 인수를 ByteArrayModelBinder에 매핑합니다.The ByteArrayModelBinderProvider which implements IModelBinderProvider maps byte[] arguments to ByteArrayModelBinder:

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        return new ByteArrayModelBinder();
    }

    return null;
}

개발자 고유의 사용자 지정 모델 바인더를 만들 때 고유의 IModelBinderProvider 형식을 구현해도 되고 ModelBinderAttribute를 사용해도 됩니다.When creating your own custom model binder, you can implement your own IModelBinderProvider type, or use the ModelBinderAttribute.

다음 예제에서는 ByteArrayModelBinder를 사용하여 base64로 인코딩된 문자열을 byte[]로 변환하고 그 결과를 파일에 저장하는 방법을 보여줍니다.The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[] and save the result to a file:

// POST: api/image
[HttpPost]
public void Post(byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}

Postman 같은 도구를 사용하여 base64로 인코딩된 문자열을 이 api 메서드에 게시할 수 있습니다.You can POST a base64-encoded string to this api method using a tool like Postman:

postmanpostman

바인더가 요청 데이터를 적절한 이름의 속성 또는 인수로 바인딩할 수 있는 한, 모델 바인딩은 성공합니다.As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. 다음 예제에서는 보기 모델에 ByteArrayModelBinder를 사용하는 방법을 보여줍니다.The following example shows how to use ByteArrayModelBinder with a view model:

[HttpPost("Profile")]
public void SaveProfile(ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);
    
    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}

사용자 지정 모델 바인더 샘플Custom model binder sample

이 섹션에서는 다음과 같은 사용자 지정 모델 바인더를 구현하겠습니다.In this section we'll implement a custom model binder that:

  • 들어오는 요청 데이터를 강력한 형식의 키 인수로 변환합니다.Converts incoming request data into strongly typed key arguments.
  • Entity Framework Core를 사용하여 연결된 엔터티를 가져옵니다.Uses Entity Framework Core to fetch the associated entity.
  • 연결된 엔터티를 작업 메서드에 인수로 전달합니다.Passes the associated entity as an argument to the action method.

다음 샘플은 Author 모델에서 ModelBinder 특성을 사용합니다.The following sample uses the ModelBinder attribute on the Author model:

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}

이전 코드에서 ModelBinder 특성은 Author 작업 매개 변수를 바인딩하는 데 사용할 IModelBinder 형식을 지정합니다.In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind Author action parameters.

다음 AuthorEntityBinder 클래스는 Entity Framework Core 및 authorId를 사용하여 데이터 원본에서 엔터티를 가져와 Author 매개 변수를 바인딩합니다.The following AuthorEntityBinder class binds an Author parameter by fetching the entity from a data source using Entity Framework Core and an authorId:

public class AuthorEntityBinder : IModelBinder
{
    private readonly AppDbContext _db;
    public AuthorEntityBinder(AppDbContext db)
    {
        _db = db;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;
        
        // Try to fetch the value of the argument by name
        var valueProviderResult =
            bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName,
            valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        int id = 0;
        if (!int.TryParse(value, out id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                                    modelName,
                                    "Author Id must be an integer.");
            return Task.CompletedTask;
        }

        // Model will be null if not found, including for 
        // out of range id values (0, -3, etc.)
        var model = _db.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

참고

앞의 AuthorEntityBinder 클래스는 사용자 지정 모델 바인더를 설명하기 위한 것입니다.The preceding AuthorEntityBinder class is intended to illustrate a custom model binder. 이 클래스는 조회 시나리오의 모범 사례를 설명하기 위한 것이 아닙니다.The class isn't intended to illustrate best practices for a lookup scenario. 조회의 경우 authorId를 바인딩하고 작업 메서드에서 데이터베이스를 쿼리합니다.For lookup, bind the authorId and query the database in an action method. 이 방식은 모델 바인딩 실패를 NotFound 사례와 구분합니다.This approach separates model binding failures from NotFound cases.

다음 코드는 작업 메서드에 AuthorEntityBinder를 사용하는 방법을 보여줍니다.The following code shows how to use the AuthorEntityBinder in an action method:

[HttpGet("get/{authorId}")]
public IActionResult Get(Author author)
{
    return Ok(author);
}

ModelBinder 특성은 기본 규칙을 사용하지 않는 매개 변수에 AuthorEntityBinder를 적용하는 데 사용할 수 있습니다.The ModelBinder attribute can be used to apply the AuthorEntityBinder to parameters that don't use default conventions:

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")]Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    return Ok(author);
}

이 예에서는 인수 이름이 기본 authorId가 아니기 때문에 ModelBinder 특성을 사용하여 매개 변수에 지정됩니다.In this example, since the name of the argument isn't the default authorId, it's specified on the parameter using the ModelBinder attribute. 컨트롤러와 작업 메서드 모두 작업 메서드에서 엔터티를 조회하는 것에 비해 간단합니다.Both the controller and action method are simplified compared to looking up the entity in the action method. Entity Framework Core를 사용하여 작성자를 가져오는 논리는 모델 바인더로 이동되었습니다.The logic to fetch the author using Entity Framework Core is moved to the model binder. Author 모델에 바인딩하는 메서드가 여러 개 있는 경우 상당히 간소화될 수 있습니다.This can be a considerable simplification when you have several methods that bind to the Author model.

개별 모델 속성(viewmodel처럼) 또는 작업 메서드 매개 변수에 ModelBinder 특성을 적용하여 그 형식 또는 작업에만 해당하는 특정 모델 바인더 또는 모델을 지정할 수 있습니다.You can apply the ModelBinder attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

ModelBinderProvider 구현Implementing a ModelBinderProvider

특성을 적용하는 대신, IModelBinderProvider를 구현할 수도 있습니다.Instead of applying an attribute, you can implement IModelBinderProvider. 기본 제공 프레임워크 바인더는 이 방식으로 구현됩니다.This is how the built-in framework binders are implemented. 바인더가 작동하는 형식을 지정할 때 바인더가 허용하는 입력이 아니라 바인더가 생성하는 인수 형식을 지정합니다.When you specify the type your binder operates on, you specify the type of argument it produces, not the input your binder accepts. 다음 바인더 공급자는 AuthorEntityBinder를 작업합니다.The following binder provider works with the AuthorEntityBinder. 공급자의 MVC 컬렉션에 추가할 때 Author 또는 Author-형식 매개 변수에서 ModelBinder 특성을 사용할 필요가 없습니다.When it's added to MVC's collection of providers, you don't need to use the ModelBinder attribute on Author or Author-typed parameters.

using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}

참고: 앞의 코드는 BinderTypeModelBinder를 반환합니다.Note: The preceding code returns a BinderTypeModelBinder. BinderTypeModelBinder는 모델 바인더에 대한 팩터리 역할을 하며 DI(종속성 주입)를 제공합니다.BinderTypeModelBinder acts as a factory for model binders and provides dependency injection (DI). AuthorEntityBinder는 EF Core에 액세스하려면 DI가 필요합니다.The AuthorEntityBinder requires DI to access EF Core. 모델 바인더에서 DI의 서비스를 요구하는 경우 BinderTypeModelBinder를 사용합니다.Use BinderTypeModelBinder if your model binder requires services from DI.

사용자 지정 모델 바인더 공급자를 사용하려면 ConfigureServices에서 추가합니다.To use a custom model binder provider, add it in ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase());

    services.AddMvc(options =>
    {
        // add custom binder to beginning of collection
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

모델 바인더를 평가할 때 공급자 컬렉션은 순서대로 검사됩니다.When evaluating model binders, the collection of providers is examined in order. 바인더를 반환하는 첫 번째 공급자가 사용됩니다.The first provider that returns a binder is used.

다음 이미지는 디버거의 기본 모델 바인더를 보여줍니다.The following image shows the default model binders from the debugger.

기본 모델 바인더default model binders

컬렉션 끝에 공급자를 추가하면 사용자 지정 바인더가 기회를 얻기도 전에 기본 제공 모델 바인더가 호출될 수 있습니다.Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. 이 예제에서는 Author 작업 인수에 사용되도록 사용자 지정 공급자를 컬렉션의 시작 부분에 추가합니다.In this example, the custom provider is added to the beginning of the collection to ensure it's used for Author action arguments.

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase());

    services.AddMvc(options =>
    {
        // add custom binder to beginning of collection
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

다형 모델 바인딩Polymorphic model binding

파생 형식의 다른 모델에 바인딩하는 것을 다형 모델 바인딩이라고 합니다.Binding to different models of derived types is known as polymorphic model binding. 요청 값을 특정 파생 모델 형식에 바인딩해야 하는 경우 다형 사용자 지정 모델 바인딩이 필요합니다.Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. 다형 모델 바인딩에는 다음과 같은 특성이 있습니다.Polymorphic model binding:

  • 모든 언어와 상호 운용할 수 있도록 설계된 REST API에는 일반적이지 않습니다.Isn't typical for a REST API that's designed to interoperate with all languages.
  • 바인딩된 모델에 대해 추론하기 어려워집니다.Makes it difficult to reason about the bound models.

그러나 앱에 다형 모델 바인딩이 필요한 경우 구현은 다음 코드와 비슷합니다.However, if an app requires polymorphic model binding, an implementation might look like the following code:

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

권장 사항 및 모범 사례Recommendations and best practices

사용자 지정 모델 바인더:Custom model binders:

  • 상태 코드를 설정하거나 결과를 반환하려고 시도하면 안 됩니다(예를 들어 404 찾을 수 없음).Shouldn't attempt to set status codes or return results (for example, 404 Not Found). 모델 바인딩이 실패하면 작업 필터 또는 작업 메서드 자체의 논리에서 오류를 처리해야 합니다.If model binding fails, an action filter or logic within the action method itself should handle the failure.
  • 작업 메서드에서 반복 코드와 교차 편집 문제를 제거하는 데 가장 유용합니다.Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
  • 일반적으로 문자열을 사용자 지정 형식으로 변환하는 데 사용되지 않습니다. TypeConverter가 더 좋은 옵션입니다.Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.