Powiązanie modelu niestandardowego w ASP.NET Core

Przez Steve Smith i Kirk Larkin

Powiązanie modelu umożliwia działanie akcji kontrolera bezpośrednio z typami modelu (przekazywanymi jako argumenty metody), a nie żądaniami HTTP. Mapowanie danych przychodzących żądań i modeli aplikacji jest obsługiwane przez powiązania modelu. Deweloperzy mogą rozszerzyć wbudowaną funkcjonalność powiązania modelu, implementując niestandardowe powiązania modelu (zazwyczaj nie trzeba pisać własnego dostawcy).

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Domyślne ograniczenia powiązania modelu

Domyślne powiązania modelu obsługują większość typowych typów danych platformy .NET Core i powinny spełniać potrzeby większości deweloperów. Oczekują powiązania danych wejściowych opartych na tekście bezpośrednio z żądania do typów modeli. Przed powiązaniem może być konieczne przekształcenie danych wejściowych. Na przykład jeśli masz klucz, który może służyć do wyszukiwania danych modelu. Możesz użyć niestandardowego powiązania modelu, aby pobrać dane na podstawie klucza.

Przegląd powiązania modelu

Powiązanie modelu używa określonych definicji dla typów, na których działa. Prosty typ jest konwertowany z jednego ciągu w danych wejściowych. Typ złożony jest konwertowany z wielu wartości wejściowych. Struktura określa różnicę na podstawie istnienia obiektu TypeConverter. Zalecamy utworzenie konwertera typów, jeśli masz proste string —>SomeType mapowanie, które nie wymaga zasobów zewnętrznych.

Przed utworzeniem własnego powiązania modelu niestandardowego warto zapoznać się z implementacją istniejących powiązań modelu. ByteArrayModelBinder Należy wziąć pod uwagę, których można użyć do konwertowania ciągów zakodowanych w formacie base64 na tablice bajtów. Tablice bajtów są często przechowywane jako pliki lub pola obiektu BLOB bazy danych.

Praca z elementem ByteArrayModelBinder

Ciągi zakodowane w formacie Base64 mogą służyć do reprezentowania danych binarnych. Na przykład obraz może być zakodowany jako ciąg. Przykład zawiera obraz jako ciąg zakodowany w formacie base64 w Base64String.txt.

ASP.NET Core MVC może przyjąć ciąg zakodowany w formacie base64 i użyć elementu , ByteArrayModelBinder aby przekonwertować go na tablicę bajtów. Argumenty ByteArrayModelBinderProvider mapowania byte[] na 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;
}

Podczas tworzenia własnego powiązania modelu niestandardowego można zaimplementować własny IModelBinderProvider typ lub użyć elementu ModelBinderAttribute.

W poniższym przykładzie pokazano, jak przekonwertować ByteArrayModelBinder ciąg zakodowany w formacie base64 na byte[] element i zapisać wynik w pliku:

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

Jeśli chcesz zobaczyć komentarze kodu przetłumaczone na języki inne niż angielski, daj nam znać w tym problemie z dyskusją w usłudze GitHub.

Ciąg zakodowany w formacie base64 można opublikować w tej metodzie interfejsu API przy użyciu narzędzia takiego jak Postman:

Postman tool

Tak długo, jak binder może powiązać dane z odpowiednio nazwanych właściwościami lub argumentami, powiązanie modelu powiedzie się. W poniższym przykładzie pokazano, jak używać z ByteArrayModelBinder modelem widoku:

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

Przykład powiązania modelu niestandardowego

W tej sekcji zaimplementujemy niestandardowy binder modelu, który:

  • Konwertuje dane żądań przychodzących na silnie typizowane argumenty klucza.
  • Używa platformy Entity Framework Core do pobierania skojarzonej jednostki.
  • Przekazuje skojarzona jednostkę jako argument do metody akcji.

W poniższym przykładzie użyto atrybutu ModelBinder w Author modelu:

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

W poprzednim kodzie ModelBinder atrybut określa typ IModelBinder , który ma być używany do powiązania Author parametrów akcji.

Poniższa AuthorEntityBinder klasa wiąże Author parametr, pobierając jednostkę ze źródła danych przy użyciu platformy Entity Framework Core i klasy 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;
    }
}

Uwaga

Poprzednia AuthorEntityBinder klasa ma na celu zilustrowanie niestandardowego powiązania modelu. Klasa nie jest przeznaczona do zilustrowania najlepszych rozwiązań dla scenariusza wyszukiwania. W przypadku wyszukiwania powiąż authorId bazę danych i wykonaj zapytanie względem bazy danych w metodzie akcji. Takie podejście oddziela błędy powiązań modelu od NotFound przypadków.

Poniższy kod pokazuje, jak używać AuthorEntityBinder metody w metodzie akcji:

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

    return Ok(author);
}

Atrybut ModelBinder może służyć do stosowania AuthorEntityBinder parametrów, które nie używają konwencji domyślnych:

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

    return Ok(author);
}

W tym przykładzie, ponieważ nazwa argumentu nie jest wartością domyślną authorId, jest określona na parametrze przy użyciu atrybutu ModelBinder . Zarówno kontroler, jak i metoda akcji są uproszczone w porównaniu do wyszukiwania jednostki w metodzie akcji. Logika pobierania autora przy użyciu platformy Entity Framework Core jest przenoszona do powiązania modelu. Może to być znaczne uproszczenie, gdy istnieje kilka metod, które wiążą się z modelem Author .

Atrybut można zastosować ModelBinder do poszczególnych właściwości modelu (np. w modelu viewmodel) lub do parametrów metody akcji, aby określić określony binder modelu lub nazwę modelu tylko dla tego typu lub akcji.

Implementowanie obiektu ModelBinderProvider

Zamiast stosować atrybut, można zaimplementować IModelBinderProviderelement . W ten sposób są implementowane wbudowane powiązania struktury. Po określeniu typu, na którym działa binder, należy określić typ generowanego argumentu, a nie dane wejściowe, które akceptuje binder. Następujący dostawca binder współpracuje z programem AuthorEntityBinder. Po dodaniu do kolekcji dostawców MVC nie trzeba używać atrybutu ModelBinder w parametrach typu Author ani 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;
        }
    }
}

Uwaga: powyższy kod zwraca wartość BinderTypeModelBinder. BinderTypeModelBinder działa jako fabryka powiązań modeli i zapewnia wstrzykiwanie zależności (DI). Wymaga AuthorEntityBinder di dostępu do platformy EF Core. Użyj BinderTypeModelBinder , jeśli binder modelu wymaga usług z di.

Aby użyć niestandardowego dostawcy powiązania modelu, dodaj go w pliku ConfigureServices:

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

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

Podczas oceniania powiązań modelu kolekcja dostawców jest badana w kolejności. Pierwszy dostawca, który zwraca powiązanie zgodne z modelem wejściowym, jest używany. Dodanie dostawcy na końcu kolekcji może w związku z tym spowodować wywołanie wbudowanego powiązania modelu, zanim niestandardowy binder będzie miał szansę. W tym przykładzie dostawca niestandardowy jest dodawany na początku kolekcji, aby upewnić się, że jest on zawsze używany do Author argumentów akcji.

Powiązanie modelu polimorficznego

Powiązanie z różnymi modelami typów pochodnych jest nazywane powiązaniem modelu polimorficznego. Powiązanie modelu niestandardowego polimorficznego jest wymagane, gdy wartość żądania musi być powiązana z określonym typem modelu pochodnego. Powiązanie modelu polimorficznego:

  • Nie jest typowe dla interfejsu REST API, który jest przeznaczony do współdziałania ze wszystkimi językami.
  • Utrudnia rozumowanie o powiązanych modelach.

Jeśli jednak aplikacja wymaga powiązania modelu polimorficznego, implementacja może wyglądać podobnie do następującego kodu:

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

Rekomendacje i najlepsze rozwiązania

Niestandardowe powiązania modelu:

  • Nie należy próbować ustawiać kodów stanu ani zwracać wyników (na przykład 404 Nie znaleziono). Jeśli powiązanie modelu zakończy się niepowodzeniem, filtr akcji lub logika w samej metodzie akcji powinien obsłużyć błąd.
  • Najbardziej przydatne w przypadku eliminowania powtarzających się problemów związanych z kodem i krzyżowymi problemami z metodami akcji.
  • Zazwyczaj nie należy używać do konwertowania ciągu na typ niestandardowy, TypeConverter zazwyczaj jest to lepsza opcja.

Przez Steve Smith

Powiązanie modelu umożliwia działanie akcji kontrolera bezpośrednio z typami modelu (przekazywanymi jako argumenty metody), a nie żądaniami HTTP. Mapowanie danych przychodzących żądań i modeli aplikacji jest obsługiwane przez powiązania modelu. Deweloperzy mogą rozszerzyć wbudowaną funkcjonalność powiązania modelu, implementując niestandardowe powiązania modelu (zazwyczaj nie trzeba pisać własnego dostawcy).

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Domyślne ograniczenia powiązania modelu

Domyślne powiązania modelu obsługują większość typowych typów danych platformy .NET Core i powinny spełniać potrzeby większości deweloperów. Oczekują powiązania danych wejściowych opartych na tekście bezpośrednio z żądania do typów modeli. Przed powiązaniem może być konieczne przekształcenie danych wejściowych. Na przykład jeśli masz klucz, który może służyć do wyszukiwania danych modelu. Możesz użyć niestandardowego powiązania modelu, aby pobrać dane na podstawie klucza.

Przegląd powiązania modelu

Powiązanie modelu używa określonych definicji dla typów, na których działa. Prosty typ jest konwertowany z jednego ciągu w danych wejściowych. Typ złożony jest konwertowany z wielu wartości wejściowych. Struktura określa różnicę na podstawie istnienia obiektu TypeConverter. Zalecamy utworzenie konwertera typów, jeśli masz proste string —>SomeType mapowanie, które nie wymaga zasobów zewnętrznych.

Przed utworzeniem własnego niestandardowego powiązania modelu warto zapoznać się z implementacją istniejących powiązań modelu. Należy wziąć pod uwagę, ByteArrayModelBinder których można użyć do konwertowania ciągów zakodowanych w formacie base64 na tablice bajtów. Tablice bajtów są często przechowywane jako pliki lub pola obiektów BLOB bazy danych.

Praca z modelem ByteArrayModelBinder

Ciągi zakodowane w formacie Base64 mogą służyć do reprezentowania danych binarnych. Na przykład obraz może być zakodowany jako ciąg. Przykład zawiera obraz jako ciąg zakodowany w formacie base64 w Base64String.txt.

ASP.NET Core MVC może przyjąć ciąg zakodowany w formacie base64 i użyć obiektu , ByteArrayModelBinder aby przekonwertować go na tablicę bajtów. Argumenty ByteArrayModelBinderProvider mapowania byte[] na 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;
}

Podczas tworzenia własnego niestandardowego powiązania modelu można zaimplementować własny IModelBinderProvider typ lub użyć elementu ModelBinderAttribute.

W poniższym przykładzie pokazano, jak przekonwertować ByteArrayModelBinder ciąg zakodowany w formacie Base64 na element byte[] i zapisać wynik w pliku:

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

Ciąg zakodowany w formacie Base64 można opublikować w tej metodzie interfejsu API przy użyciu narzędzia takiego jak Postman:

Postman tool output

Tak długo, jak binder może powiązać dane żądania z odpowiednio nazwanych właściwości lub argumentów, powiązanie modelu powiedzie się. W poniższym przykładzie pokazano, jak używać z ByteArrayModelBinder modelem widoku:

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

Przykład niestandardowego powiązania modelu

W tej sekcji zaimplementujemy niestandardowy binder modelu, który:

  • Konwertuje dane żądań przychodzących na silnie typizowane argumenty klucza.
  • Używa programu Entity Framework Core do pobierania skojarzonej jednostki.
  • Przekazuje skojarzona jednostka jako argument do metody akcji.

W poniższym przykładzie użyto atrybutu ModelBinderAuthor w modelu:

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

W poprzednim kodzie atrybut określa typIModelBinder, ModelBinder którego należy użyć do powiązania Author parametrów akcji.

Poniższa AuthorEntityBinder klasa wiąże Author parametr przez pobranie jednostki ze źródła danych przy użyciu platformy Entity Framework Core i klasy 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;
    }
}

Uwaga

AuthorEntityBinder Poprzednia klasa ma na celu zilustrowanie niestandardowego powiązania modelu. Klasa nie jest przeznaczona do zilustrowania najlepszych rozwiązań dla scenariusza wyszukiwania. W przypadku wyszukiwania powiąż authorId bazę danych i wykonaj zapytanie względem jej w metodzie akcji. Takie podejście oddziela błędy powiązań modelu od NotFound przypadków.

Poniższy kod pokazuje, jak używać AuthorEntityBinder metody w metodzie akcji:

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

Atrybut ModelBinder może służyć do zastosowania AuthorEntityBinder do parametrów, które nie używają konwencji domyślnych:

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

    return Ok(author);
}

W tym przykładzie, ponieważ nazwa argumentu nie jest wartością domyślną authorId, jest określona w parametrze przy użyciu atrybutu ModelBinder . Zarówno kontroler, jak i metoda akcji są uproszczone w porównaniu do wyszukiwania jednostki w metodzie akcji. Logika pobierania autora przy użyciu platformy Entity Framework Core jest przenoszona do bindera modelu. Może to być znaczne uproszczenie, jeśli istnieje kilka metod, które wiążą się z modelem Author .

Atrybut można zastosować ModelBinder do poszczególnych właściwości modelu (takich jak model widoku) lub do parametrów metody akcji w celu określenia określonego powiązania modelu lub nazwy modelu tylko dla tego typu lub akcji.

Implementowanie klasy ModelBinderProvider

Zamiast stosować atrybut, można zaimplementować IModelBinderProvider. W ten sposób są implementowane wbudowane powiązania struktury. Po określeniu typu, na którym działa powiązanie, należy określić typ argumentu, który generuje, a nie dane wejściowe akceptowane przez powiązanie. Następujący dostawca binder współpracuje z programem AuthorEntityBinder. Po dodaniu jej do kolekcji dostawców MVC nie trzeba używać atrybutu ModelBinder w parametrach typu ani 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;
        }
    }
}

Uwaga: Powyższy kod zwraca wartość BinderTypeModelBinder. BinderTypeModelBinder działa jako fabryka powiązań modelu i zapewnia wstrzykiwanie zależności (DI). Wymaga di AuthorEntityBinder dostępu do platformy EF Core. Użyj BinderTypeModelBinder , jeśli binder modelu wymaga usług z di.

Aby użyć niestandardowego dostawcy powiązania modelu, dodaj go w pliku 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);
}

Podczas oceniania powiązań modelu kolekcja dostawców jest badana w kolejności. Pierwszy dostawca, który zwraca powiązanie, jest używany. Dodanie dostawcy na końcu kolekcji może spowodować wywołanie wbudowanego powiązania modelu, zanim niestandardowy binder będzie miał szansę. W tym przykładzie dostawca niestandardowy jest dodawany na początku kolekcji, aby upewnić się, że jest używany do Author argumentów akcji.

Powiązanie modelu polimorficznego

Powiązanie z różnymi modelami typów pochodnych jest nazywane powiązaniem modelu polimorficznego. Powiązanie modelu niestandardowego polimorficznego jest wymagane, gdy wartość żądania musi być powiązana z określonym typem modelu pochodnego. Powiązanie modelu polimorficznego:

  • Nie jest typowe dla interfejsu REST API, który jest przeznaczony do współdziałania ze wszystkimi językami.
  • Utrudnia wnioskowanie o powiązanych modelach.

Jeśli jednak aplikacja wymaga powiązania modelu polimorficznego, implementacja może wyglądać podobnie do następującego kodu:

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

Rekomendacje i najlepsze rozwiązania

Niestandardowe powiązania modelu:

  • Nie należy próbować ustawiać kodów stanu ani zwracać wyników (na przykład 404 Nie znaleziono). Jeśli powiązanie modelu nie powiedzie się, filtr akcji lub logika w samej metodzie akcji powinien obsługiwać błąd.
  • Są najbardziej przydatne w przypadku eliminowania powtarzających się problemów z kodem i krzyżowego rozwiązywania problemów z metodami akcji.
  • Zazwyczaj nie należy używać do konwertowania ciągu na typ niestandardowy, a TypeConverter zazwyczaj jest to lepsza opcja.