ASP.NET Core의 사용자 지정 모델 바인딩

Kirk Larkin

모델 바인딩을 통해 컨트롤러 작업에서 HTTP 요청이 아닌 모델 형식(메서드 인수로 전달된)을 직접 작업할 수 있습니다. 들어오는 요청 데이터와 애플리케이션 모델 간의 매핑은 모델 바인더를 통해 처리됩니다. 개발자는 사용자 지정 모델 바인더를 구현하여 기본 모델 바인딩 기능을 확장할 수 있습니다(일반적으로 개발자가 고유의 공급자를 작성할 필요는 없음).

샘플 코드 보기 및 다운로드(다운로드 방법)

기본 모델 바인더 제한 사항

기본 모델 바인더는 대부분의 공용 .NET Core 데이터 형식을 지원하며 대부분의 개발자 요구 사항을 충족해야 합니다. 요청의 텍스트 기반 입력을 모델 형식에 직접 바인딩할 것으로 예상됩니다. 바인딩하려면 입력을 변환해야 할 수도 있습니다. 예를 들어 모델 데이터를 조회하는 데 사용할 수 있는 키를 갖고 있는 경우 그 키를 기반으로 사용자 지정 모델 바인더를 사용하여 데이터를 가져올 수 있습니다.

단순 및 복합 형식 모델 바인딩

모델 바인딩은 작업을 수행하는 형식에 대한 특정 정의를 사용합니다. 단순 형식은 사용하거나 TryParse 메서드를 사용하여 TypeConverter 단일 문자열에서 변환됩니다. 복합 형식은 여러 입력 값에서 변환됩니다. 프레임워크는 TypeConverter 또는 TryParse 존재 여부에 따라 차이를 결정합니다. 형식 변환기를 만들거나 외부 리소스 또는 여러 입력이 필요하지 않은 SomeType 변환에 string을 위한 TryParse를 사용하는 것이 좋습니다.

모델 바인더가 문자열에서 변환할 수 있는 형식 목록은 단순 형식을 참조하세요.

개발자 고유의 사용자 지정 모델 바인더를 만들기 전에 기존 모델 바인더가 구현되는 방식을 검토하는 것이 좋습니다. base64로 인코딩된 문자열을 바이트 배열로 변환하는 데 사용할 수 있는 ByteArrayModelBinder를 고려해 보세요. 바이트 배열은 종종 파일 또는 데이터베이스 BLOB 필드로 저장됩니다.

ByteArrayModelBinder 사용

Base64로 인코딩된 문자열은 이진 데이터를 나타내는 데 사용할 수 있습니다. 예를 들어 이미지를 문자열로 인코딩할 수 있습니다. 이 샘플에는 Base64String.txt에 base64로 인코딩된 문자열로 이미지가 포함되어 있습니다.

ASP.NET Core MVC는 base64로 인코드된 문자열을 가져온 후 ByteArrayModelBinder를 사용하여 바이트 배열로 변환할 수 있습니다. ByteArrayModelBinderProviderbyte[] 인수를 ByteArrayModelBinder에 매핑합니다.

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

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
        return new ByteArrayModelBinder(loggerFactory);
    }

    return null;
}

개발자 고유의 사용자 지정 모델 바인더를 만들 때 고유의 IModelBinderProvider 형식을 구현해도 되고 ModelBinderAttribute를 사용해도 됩니다.

다음 예제에서는 ByteArrayModelBinder를 사용하여 base64로 인코딩된 문자열을 byte[]로 변환하고 그 결과를 파일에 저장하는 방법을 보여줍니다.

[HttpPost]
public void Post([FromForm] 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);
}

영어 이외의 언어로 번역된 코드 주석을 보려면 이 GitHub 토론 이슈에서 알려주세요.

curl과 같은 도구를 사용하여 base64로 인코딩된 문자열을 이전 api 메서드에 게시할 수 있습니다.

바인더가 요청 데이터를 적절한 이름의 속성 또는 인수로 바인딩할 수 있는 한, 모델 바인딩은 성공합니다. 다음 예제에서는 보기 모델에 ByteArrayModelBinder를 사용하는 방법을 보여줍니다.

[HttpPost("Profile")]
public void SaveProfile([FromForm] 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; }
}

사용자 지정 모델 바인더 샘플

이 섹션에서는 다음과 같은 사용자 지정 모델 바인더를 구현하겠습니다.

  • 들어오는 요청 데이터를 강력한 형식의 키 인수로 변환합니다.
  • Entity Framework Core를 사용하여 연결된 엔터티를 가져옵니다.
  • 연결된 엔터티를 작업 메서드에 인수로 전달합니다.

다음 샘플은 Author 모델에서 ModelBinder 특성을 사용합니다.

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

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 형식을 지정합니다.

다음 AuthorEntityBinder 클래스는 Entity Framework Core 및 authorId를 사용하여 데이터 원본에서 엔터티를 가져와 Author 매개 변수를 바인딩합니다.

public class AuthorEntityBinder : IModelBinder
{
    private readonly AuthorContext _context;

    public AuthorEntityBinder(AuthorContext context)
    {
        _context = context;
    }

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

        if (!int.TryParse(value, out var 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 = _context.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

참고 항목

앞의 AuthorEntityBinder 클래스는 사용자 지정 모델 바인더를 설명하기 위한 것입니다. 이 클래스는 조회 시나리오의 모범 사례를 설명하기 위한 것이 아닙니다. 조회의 경우 authorId를 바인딩하고 작업 메서드에서 데이터베이스를 쿼리합니다. 이 방식은 모델 바인딩 실패를 NotFound 사례와 구분합니다.

다음 코드는 작업 메서드에 AuthorEntityBinder를 사용하는 방법을 보여줍니다.

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

ModelBinder 특성은 기본 규칙을 사용하지 않는 매개 변수에 AuthorEntityBinder를 적용하는 데 사용할 수 있습니다.

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

이 예에서는 인수 이름이 기본 authorId가 아니기 때문에 ModelBinder 특성을 사용하여 매개 변수에 지정됩니다. 컨트롤러와 작업 메서드 모두 작업 메서드에서 엔터티를 조회하는 것에 비해 간단합니다. Entity Framework Core를 사용하여 작성자를 가져오는 논리는 모델 바인더로 이동되었습니다. Author 모델에 바인딩하는 메서드가 여러 개 있는 경우 상당히 간소화될 수 있습니다.

개별 모델 속성(viewmodel처럼) 또는 작업 메서드 매개 변수에 ModelBinder 특성을 적용하여 그 형식 또는 작업에만 해당하는 특정 모델 바인더 또는 모델을 지정할 수 있습니다.

ModelBinderProvider 구현

특성을 적용하는 대신, IModelBinderProvider를 구현할 수도 있습니다. 기본 제공 프레임워크 바인더는 이 방식으로 구현됩니다. 바인더가 작동하는 형식을 지정할 때 바인더가 허용하는 입력이 아니라 바인더가 생성하는 인수 형식을 지정합니다. 다음 바인더 공급자는 AuthorEntityBinder를 작업합니다. 공급자의 MVC 컬렉션에 추가할 때 Author 또는 Author-형식 매개 변수에서 ModelBinder 특성을 사용할 필요가 없습니다.

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를 반환합니다. BinderTypeModelBinder는 모델 바인더에 대한 팩터리 역할을 하며 DI(종속성 주입)를 제공합니다. AuthorEntityBinder는 EF Core에 액세스하려면 DI가 필요합니다. 모델 바인더에서 DI의 서비스를 요구하는 경우 BinderTypeModelBinder를 사용합니다.

사용자 지정 모델 바인더 공급자를 사용하려면 ConfigureServices에서 추가합니다.

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

    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

모델 바인더를 평가할 때 공급자 컬렉션은 순서대로 검사됩니다. 입력 모델과 일치하는 바인더를 반환하는 첫 번째 공급자가 사용됩니다. 따라서 컬렉션 끝에 공급자를 추가하면 사용자 지정 바인더가 사용되기 전에 기본 제공 모델 바인더가 호출될 수 있습니다. 이 예제에서 사용자 지정 공급자는 Author 작업 인수에 항상 사용되도록 컬렉션의 시작 부분에 추가됩니다.

다형 모델 바인딩

파생 형식의 다른 모델에 바인딩하는 것을 다형 모델 바인딩이라고 합니다. 요청 값을 특정 파생 모델 형식에 바인딩해야 하는 경우 다형 사용자 지정 모델 바인딩이 필요합니다. 다형 모델 바인딩에는 다음과 같은 특성이 있습니다.

  • 모든 언어와 상호 운용할 수 있도록 설계된 REST API에는 일반적이지 않습니다.
  • 바인딩된 모델에 대해 추론하기 어려워집니다.

그러나 앱에 다형 모델 바인딩이 필요한 경우 구현은 다음 코드와 비슷합니다.

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.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

권장 사항 및 모범 사례

사용자 지정 모델 바인더:

  • 상태 코드를 설정하거나 결과를 반환하려고 시도하면 안 됩니다(예를 들어 404 찾을 수 없음). 모델 바인딩이 실패하면 작업 필터 또는 작업 메서드 자체의 논리에서 오류를 처리해야 합니다.
  • 작업 메서드에서 반복 코드와 교차 편집 문제를 제거하는 데 가장 유용합니다.
  • 일반적으로 문자열을 사용자 지정 형식으로 변환하는 데 사용되지 않습니다. TypeConverter가 더 좋은 옵션입니다.

작성자 Steve Smith

모델 바인딩을 통해 컨트롤러 작업에서 HTTP 요청이 아닌 모델 형식(메서드 인수로 전달된)을 직접 작업할 수 있습니다. 들어오는 요청 데이터와 애플리케이션 모델 간의 매핑은 모델 바인더를 통해 처리됩니다. 개발자는 사용자 지정 모델 바인더를 구현하여 기본 모델 바인딩 기능을 확장할 수 있습니다(일반적으로 개발자가 고유의 공급자를 작성할 필요는 없음).

샘플 코드 보기 및 다운로드(다운로드 방법)

기본 모델 바인더 제한 사항

기본 모델 바인더는 대부분의 공용 .NET Core 데이터 형식을 지원하며 대부분의 개발자 요구 사항을 충족해야 합니다. 요청의 텍스트 기반 입력을 모델 형식에 직접 바인딩할 것으로 예상됩니다. 바인딩하려면 입력을 변환해야 할 수도 있습니다. 예를 들어 모델 데이터를 조회하는 데 사용할 수 있는 키를 갖고 있는 경우 그 키를 기반으로 사용자 지정 모델 바인더를 사용하여 데이터를 가져올 수 있습니다.

모델 바인딩 검토

모델 바인딩은 작업을 수행하는 형식에 대한 특정 정의를 사용합니다. 단순 형식은 입력의 단일 문자열에서 변환됩니다. 복합 형식은 여러 입력 값에서 변환됩니다. 프레임워크는 TypeConverter의 존재 여부에 따라 차이점을 확인합니다. 외부 리소스가 필요 없는 간단한 string ->SomeType 매핑이 있는 경우 형식 변환기를 만드는 것이 좋습니다.

개발자 고유의 사용자 지정 모델 바인더를 만들기 전에 기존 모델 바인더가 구현되는 방식을 검토하는 것이 좋습니다. base64로 인코딩된 문자열을 바이트 배열로 변환하는 데 사용할 수 있는 ByteArrayModelBinder를 고려해 보세요. 바이트 배열은 종종 파일 또는 데이터베이스 BLOB 필드로 저장됩니다.

ByteArrayModelBinder 사용

Base64로 인코딩된 문자열은 이진 데이터를 나타내는 데 사용할 수 있습니다. 예를 들어 이미지를 문자열로 인코딩할 수 있습니다. 이 샘플에는 Base64String.txt에 base64로 인코딩된 문자열로 이미지가 포함되어 있습니다.

ASP.NET Core MVC는 base64로 인코드된 문자열을 가져온 후 ByteArrayModelBinder를 사용하여 바이트 배열로 변환할 수 있습니다. ByteArrayModelBinderProviderbyte[] 인수를 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를 사용해도 됩니다.

다음 예제에서는 ByteArrayModelBinder를 사용하여 base64로 인코딩된 문자열을 byte[]로 변환하고 그 결과를 파일에 저장하는 방법을 보여줍니다.

[HttpPost]
public void Post([FromForm] 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);
}

curl과 같은 도구를 사용하여 base64로 인코딩된 문자열을 이전 api 메서드에 게시할 수 있습니다.

바인더가 요청 데이터를 적절한 이름의 속성 또는 인수로 바인딩할 수 있는 한, 모델 바인딩은 성공합니다. 다음 예제에서는 보기 모델에 ByteArrayModelBinder를 사용하는 방법을 보여줍니다.

[HttpPost("Profile")]
public void SaveProfile([FromForm] 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; }
}

사용자 지정 모델 바인더 샘플

이 섹션에서는 다음과 같은 사용자 지정 모델 바인더를 구현하겠습니다.

  • 들어오는 요청 데이터를 강력한 형식의 키 인수로 변환합니다.
  • Entity Framework Core를 사용하여 연결된 엔터티를 가져옵니다.
  • 연결된 엔터티를 작업 메서드에 인수로 전달합니다.

다음 샘플은 Author 모델에서 ModelBinder 특성을 사용합니다.

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

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 형식을 지정합니다.

다음 AuthorEntityBinder 클래스는 Entity Framework Core 및 authorId를 사용하여 데이터 원본에서 엔터티를 가져와 Author 매개 변수를 바인딩합니다.

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

        if (!int.TryParse(value, out var 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 클래스는 사용자 지정 모델 바인더를 설명하기 위한 것입니다. 이 클래스는 조회 시나리오의 모범 사례를 설명하기 위한 것이 아닙니다. 조회의 경우 authorId를 바인딩하고 작업 메서드에서 데이터베이스를 쿼리합니다. 이 방식은 모델 바인딩 실패를 NotFound 사례와 구분합니다.

다음 코드는 작업 메서드에 AuthorEntityBinder를 사용하는 방법을 보여줍니다.

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    
    return Ok(author);
}

ModelBinder 특성은 기본 규칙을 사용하지 않는 매개 변수에 AuthorEntityBinder를 적용하는 데 사용할 수 있습니다.

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

이 예에서는 인수 이름이 기본 authorId가 아니기 때문에 ModelBinder 특성을 사용하여 매개 변수에 지정됩니다. 컨트롤러와 작업 메서드 모두 작업 메서드에서 엔터티를 조회하는 것에 비해 간단합니다. Entity Framework Core를 사용하여 작성자를 가져오는 논리는 모델 바인더로 이동되었습니다. Author 모델에 바인딩하는 메서드가 여러 개 있는 경우 상당히 간소화될 수 있습니다.

개별 모델 속성(viewmodel처럼) 또는 작업 메서드 매개 변수에 ModelBinder 특성을 적용하여 그 형식 또는 작업에만 해당하는 특정 모델 바인더 또는 모델을 지정할 수 있습니다.

ModelBinderProvider 구현

특성을 적용하는 대신, IModelBinderProvider를 구현할 수도 있습니다. 기본 제공 프레임워크 바인더는 이 방식으로 구현됩니다. 바인더가 작동하는 형식을 지정할 때 바인더가 허용하는 입력이 아니라 바인더가 생성하는 인수 형식을 지정합니다. 다음 바인더 공급자는 AuthorEntityBinder를 작업합니다. 공급자의 MVC 컬렉션에 추가할 때 Author 또는 Author-형식 매개 변수에서 ModelBinder 특성을 사용할 필요가 없습니다.

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를 반환합니다. BinderTypeModelBinder는 모델 바인더에 대한 팩터리 역할을 하며 DI(종속성 주입)를 제공합니다. AuthorEntityBinder는 EF Core에 액세스하려면 DI가 필요합니다. 모델 바인더에서 DI의 서비스를 요구하는 경우 BinderTypeModelBinder를 사용합니다.

사용자 지정 모델 바인더 공급자를 사용하려면 ConfigureServices에서 추가합니다.

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

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

모델 바인더를 평가할 때 공급자 컬렉션은 순서대로 검사됩니다. 바인더를 반환하는 첫 번째 공급자가 사용됩니다. 컬렉션 끝에 공급자를 추가하면 사용자 지정 바인더가 기회를 얻기도 전에 기본 제공 모델 바인더가 호출될 수 있습니다. 이 예제에서는 Author 작업 인수에 사용되도록 사용자 지정 공급자를 컬렉션의 시작 부분에 추가합니다.

다형 모델 바인딩

파생 형식의 다른 모델에 바인딩하는 것을 다형 모델 바인딩이라고 합니다. 요청 값을 특정 파생 모델 형식에 바인딩해야 하는 경우 다형 사용자 지정 모델 바인딩이 필요합니다. 다형 모델 바인딩에는 다음과 같은 특성이 있습니다.

  • 모든 언어와 상호 운용할 수 있도록 설계된 REST API에는 일반적이지 않습니다.
  • 바인딩된 모델에 대해 추론하기 어려워집니다.

그러나 앱에 다형 모델 바인딩이 필요한 경우 구현은 다음 코드와 비슷합니다.

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.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

권장 사항 및 모범 사례

사용자 지정 모델 바인더:

  • 상태 코드를 설정하거나 결과를 반환하려고 시도하면 안 됩니다(예를 들어 404 찾을 수 없음). 모델 바인딩이 실패하면 작업 필터 또는 작업 메서드 자체의 논리에서 오류를 처리해야 합니다.
  • 작업 메서드에서 반복 코드와 교차 편집 문제를 제거하는 데 가장 유용합니다.
  • 일반적으로 문자열을 사용자 지정 형식으로 변환하는 데 사용되지 않습니다. TypeConverter가 더 좋은 옵션입니다.