Поделиться через


Привязка пользовательской модели в ASP.NET Core

Кирк Ларкин

Привязка модели позволяет действиям контроллера работать непосредственно с типами моделей (переданными в качестве аргументов метода), а не с HTTP-запросами. Сопоставление данных входящих запросов и моделей приложений обрабатывается привязывателями моделей. Разработчики могут расширить встроенную функциональность привязки модели, реализуя пользовательские связыватели моделей (хотя, как правило, вам не потребуется писать собственный связыватель).

Просмотреть или скачать образец кода (описание загрузки)

Ограничения привязывателя модели по умолчанию

Привязки моделей по умолчанию поддерживают большинство распространенных типов данных .NET Core и должны соответствовать большинству потребностей разработчиков. Они ожидают привязки текстовых входных данных из запроса непосредственно к типам моделей. Может потребоваться преобразовать входные данные перед привязкой. Например, если у вас есть ключ, который можно использовать для поиска данных модели. Вы можете использовать пользовательский привязчик модели для получения данных на основе ключа.

Привязка модели простых и сложных типов

Для типов, с которыми работает привязка данных, используются специальные определения. Простой тип преобразуется из одной строки с помощью метода TypeConverter или метода TryParse. Сложный тип преобразуется из нескольких входных значений. Фреймворк определяет разницу на основе существования TypeConverter или TryParse. Рекомендуется создать преобразователь типов или использовать TryParse для stringSomeType преобразования, не требующего внешних ресурсов или нескольких входных данных.

См. простые типы для списка типов, которые может преобразовать привязыватель модели из строк.

Перед созданием собственного пользовательского привязчика модели следует ознакомиться с тем, как реализуются существующие привязки моделей. ByteArrayModelBinder можно использовать для преобразования строк в кодировке Base64 в массивы байтов. Массивы байтов часто хранятся в виде файлов или полей BLOB-объектов базы данных.

Работа с ByteArrayModelBinder

Строки в кодировке Base64 можно использовать для представления двоичных данных. Например, изображение можно закодировать как строку. Пример включает изображение в виде строки в кодировке Base64 в Base64String.txt.

ASP.NET Core MVC может принимать строку в кодировке Base64 и использовать ее ByteArrayModelBinder для преобразования в массив байтов. ByteArrayModelBinderProvider отображает аргументы byte[] в 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.

Вы можете отправить строку в кодировке Base64 предыдущему методу API с помощью такого инструмента, как curl.

Если привязка может привязать данные запроса к соответствующим именованным свойствам или аргументам, привязка модели будет успешно выполнена. В следующем примере показано, как использовать 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 для получения связанной сущности.
  • Передает связанную сущность в качестве аргумента методу действия.

В следующем примере используется атрибут ModelBinder на модели Author.

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 атрибут указывает тип, который следует использовать для привязки IModelBinderAuthor параметров действия.

Следующий AuthorEntityBinder класс связывает Author параметр, извлекая сущность из источника данных с помощью Entity Framework Core и authorId.

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 модели.

Вы можете применить атрибут ModelBinder к отдельным свойствам модели (например, в viewmodel) или к параметрам метода действия, чтобы указать определенный связующий компонент модели или имя модели только для этого типа или действия.

Реализация ModelBinderProvider

Вместо применения атрибута можно реализовать IModelBinderProvider. Вот как реализуются встроенные привязки фреймворка. При указании типа, с которым работает привязка, вы указываете тип создаваемого аргумента, а не входные данные, которые принимает привязка. Следующий поставщик папок работает с AuthorEntityBinder. При добавлении в коллекцию поставщиков MVC не требуется использовать атрибут ModelBinder для параметров типов Author или Author.

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). Для доступа к EF Core требуется DI для AuthorEntityBinder. Используйте, если для привязки модели требуются BinderTypeModelBinder службы из DI.

Чтобы использовать провайдер привязки пользовательской модели, добавьте его в ConfigureServices:

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

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

При оценке модельных привязок коллекция поставщиков рассматривается по порядку. Используется первый поставщик, возвращающий привязку, соответствующую входной модели. Добавление вашего провайдера в конец коллекции может привести к тому, что встроенный привязчик модели будет вызван до того, как пользовательский привязчик получит шанс. В этом примере настраиваемый поставщик добавляется в начало коллекции, чтобы убедиться, что он всегда используется для Author аргументов действия.

Привязка полиморфной модели

Привязка к различным моделям производных типов называется многоморфной привязкой модели. Привязка полиморфной кастомной модели требуется, когда значение запроса должно быть привязано к конкретному производному типу модели. Привязка полиморфной модели:

  • Не является типичным для API, который предназначен для REST взаимодействия со всеми языками.
  • Затрудняет осмысление связанных моделей.

Однако если приложению требуется многоморфная привязка модели, реализация может выглядеть следующим образом:

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 Not Found). Если привязка модели завершается сбоем, фильтр действий или логика в самом методе действия должна обрабатывать сбой.
  • Наиболее полезны для устранения повторяющихся кодов и перекрестных проблем из методов действий.
  • Как правило, не следует использовать для преобразования строки в пользовательский тип, TypeConverter обычно это лучший вариант.

Автор: Стив Смит (Steve Smith)

Привязка модели позволяет действиям контроллера работать непосредственно с типами моделей (переданными в качестве аргументов метода), а не с HTTP-запросами. Сопоставление данных входящих запросов и моделей приложений обрабатывается привязывателями моделей. Разработчики могут расширить встроенные функции привязки модели, реализуя пользовательские привязки моделей (хотя обычно вам не нужно писать собственный поставщик).

Просмотреть или скачать образец кода (описание загрузки)

Ограничения привязывателя модели по умолчанию

Привязки моделей по умолчанию поддерживают большинство распространенных типов данных .NET Core и должны соответствовать большинству потребностей разработчиков. Они ожидают привязки текстовых входных данных из запроса непосредственно к типам моделей. Может потребоваться преобразовать входные данные перед привязкой. Например, если у вас есть ключ, который можно использовать для поиска данных модели. Вы можете использовать пользовательский привязчик модели для получения данных на основе ключа.

Проверка привязки модели

Для типов, с которыми работает привязка данных, используются специальные определения. Простой тип преобразуется из одной строки во входных данных. Сложный тип преобразуется из нескольких входных значений. Фреймворк определяет разницу на основе существования TypeConverter. Рекомендуем создать преобразователь типа, если у вас есть простое сопоставление string, >, SomeType, которое не требует внешних ресурсов.

Перед созданием собственного пользовательского привязчика модели следует ознакомиться с тем, как реализуются существующие привязки моделей. ByteArrayModelBinder Рассмотрим, какие можно использовать для преобразования строк в кодировке Base64 в массивы байтов. Массивы байтов часто хранятся в виде файлов или полей BLOB-объектов базы данных.

Работа с «ByteArrayModelBinder»

Строки в кодировке Base64 можно использовать для представления двоичных данных. Например, изображение можно закодировать как строку. Пример включает изображение в виде строки в кодировке Base64 в Base64String.txt.

ASP.NET Core MVC может принимать строку в кодировке Base64 и использовать ее ByteArrayModelBinder для преобразования в массив байтов. Аргументы ByteArrayModelBinderProvider картыbyte[]: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 как преобразовать строку в кодировке byte[] Base64 в файл и сохранить результат в файле:

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

Можно ОПУБЛИКОВАТЬ строку в кодировке Base64 в предыдущем методе API с помощью средства, например curl.

Если привязка может привязать данные запроса к соответствующим именованным свойствам или аргументам, привязка модели будет успешно выполнена. В следующем примере показано, как использовать 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 для извлечения связанной сущности.
  • Передает связанную сущность в качестве аргумента методу действия.

В следующем примере для модели используется ModelBinder атрибут Author :

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 указывает тип IModelBinder, который следует использовать для привязки параметров действия Author.

AuthorEntityBinder Следующий класс привязывает Author параметр, извлекая сущность из источника данных с помощью Entity Framework Core и :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;
        }

        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 модели.

Вы можете применить атрибут ModelBinder к отдельным свойствам модели (например, во viewmodel) или к параметрам метода действия, чтобы указать конкретный привязчик модели или имя модели только для этого типа или действия.

Реализация провайдера ModelBinder

Вместо применения атрибута можно реализовать IModelBinderProvider. Вот как реализованы встроенные связыватели фреймворка. При указании типа, с которым работает привязка, вы указываете тип создаваемого аргумента, а не входные данные, которые принимает привязка. Следующий поставщик привязок работает с AuthorEntityBinder. При добавлении в коллекцию поставщиков MVC не требуется использовать атрибут ModelBinder для параметров типа Author или Author.

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 требуется DI.EF Core Используйте BinderTypeModelBinder, если для привязки модели требуются службы из DI.

Чтобы использовать провайдер привязки настраиваемой модели, добавьте его в 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.

Привязка полиморфной модели

Привязка к различным моделям производных типов называется многоморфной привязкой модели. Привязка пользовательской модели с полиморфной привязкой требуется, когда значение запроса должно быть привязано к конкретному производному типу модели. Привязка полиморфной модели:

  • Не является типичным для 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 Not Found). Если привязка модели завершается сбоем, фильтр действий или логика в самом методе действия должна обрабатывать сбой.
  • Наиболее полезны для устранения повторяющихся кодов и перекрестных проблем из методов действий.
  • Как правило, не следует использовать для преобразования строки в пользовательский тип, TypeConverter обычно это лучший вариант.