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

Автор: Кирк Ларкин (Kirk Larkin)

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

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

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

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

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

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

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

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

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

Следующий класс 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). Для AuthorEntityBinder доступа требуется di.EF Core Если связывателю модели необходимы службы из 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 аргументов действия.

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

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

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

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

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

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

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

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

Обзор привязки модели

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

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

Работа с классом 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 для преобразования строки в кодировке 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);
}

Можно ОПУБЛИКОВАТЬ строку в кодировке 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) или к параметрам метода действия.

Реализация класса 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). Для AuthorEntityBinder доступа требуется di.EF Core Если связывателю модели необходимы службы из 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.

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

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

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