Niestandardowe powiązanie modelu w programie ASP.NET Core

A ich producentami są Steve Smith i Steven Larkin

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

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

Ograniczenia domyślnego powiązania modelu

Domyślne powiązania modeli obsługują większość typowych typów danych .NET Core i powinny spełniać potrzeby większości deweloperów. Oczekują powiązania tekstowych danych wejściowych z żądania bezpośrednio z typami modeli. Może być konieczne przekształcenie danych wejściowych przed ich powiązaniem. Na przykład jeśli masz klucz, który może służyć do wyszukiwania danych modelu. Możesz użyć niestandardowego narzędzia do 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. Typ prosty jest konwertowany z pojedynczego ciągu w danych wejściowych. Typ złożony jest konwertowany z wielu wartości wejściowych. Framework określa różnicę na podstawie istnienia 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 przejrzeć sposób implementowania istniejących binderów modelu. Rozważmy wartość , która może służyć do konwertowania ciągów zakodowanych w ByteArrayModelBinder formacie base64 na tablice bajtów. Tablice bajtów są często przechowywane jako pliki lub pola obiektów BLOB bazy danych.

Praca z 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 przekonwertować go na tablicę bajtów za pomocą klasy ByteArrayModelBinder . Argumenty ByteArrayModelBinderProvider byte[] mapują 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 niestandardowego bindera modelu można zaimplementować własny typ lub IModelBinderProvider użyć . ModelBinderAttribute

W poniższym przykładzie pokazano, jak przekonwertować ciąg zakodowany w formacie base64 na ciąg i zapisać ByteArrayModelBinder byte[] 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 do kodu przetłumaczone na języki inne niż angielski, daj nam znać w tym problemie z dyskusją w serwisie GitHub.

Możesz wysłać ciąg zakodowany w formacie base64 do tej metody interfejsu API przy użyciu narzędzia takiego jak Postman:

Listonosz

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ć ByteArrayModelBinder z 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 programu binder modelu

W tej sekcji zaim implementowany jest niestandardowy binder modelu, który:

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

W poniższym przykładzie użyto ModelBinder atrybutu 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 atrybut określa typ , który ma być używany ModelBinder IModelBinder do powiązania parametrów Author akcji.

Następująca AuthorEntityBinder klasa wiąże parametr przez pobranie jednostki ze źródła danych przy użyciu Entity Framework Core Author i 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 klasa AuthorEntityBinder ma na celu zilustrowanie niestandardowego bindera modelu. Klasa nie ma na celu zilustrowania najlepszych rozwiązań dla scenariusza wyszukiwania. W przypadku wyszukiwania powiąż authorId element i odpytaj bazę danych w metodzie akcji. Takie podejście oddziela błędy powiązań modelu od NotFound przypadków.

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

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

    return Ok(author);
}

Atrybutu ModelBinder można użyć do zastosowania do AuthorEntityBinder parametrów, które nie korzystają z domyślnych konwencji:

[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 domyślną wartością , jest ona określana dla authorId parametru przy użyciu ModelBinder atrybutu . Zarówno kontroler, jak i metoda akcji są uproszczone w porównaniu do szukania jednostki w metodzie akcji. Logika pobierania autora przy użyciu Entity Framework Core jest przenoszony do bindera modelu. Może to być znaczne uproszczenie, jeśli z modelem wiąże się kilka Author metod.

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

Implementowanie modeluBinderProvider

Zamiast stosować atrybut, można zaimplementować IModelBinderProvider . W ten sposób implementowane są wbudowane struktury binderów. Po określeniu typu, na którym działa binder, należy określić typ argumentu, który generuje, a nie dane wejściowe akceptowane przez binder. Następujący dostawca binderów współpracuje z programem AuthorEntityBinder . Po dodaniu go do kolekcji dostawców MVC nie trzeba używać atrybutu na parametrach ModelBinder Author lub Author typach.

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: poprzedni kod zwraca wartość BinderTypeModelBinder . BinderTypeModelBinder działa jako fabryka dla binderów modelu i zapewnia wstrzykiwanie zależności. Aby AuthorEntityBinder uzyskać dostęp do EF Core, EF Core. Użyj BinderTypeModelBinder , jeśli twój binder modelu wymaga usług z di.

Aby użyć niestandardowego dostawcy binderów modelu, dodaj go w ConfigureServices :

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

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

Podczas oceny elementów binderów modelu kolekcja dostawców jest analizowana w kolejności. Pierwszy dostawca, który zwraca binder, który pasuje do modelu wejściowego, jest używany. Dodanie dostawcy na końcu kolekcji może w związku z tym spowodować wywoływanie 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 dla Author argumentów akcji.

Powiązanie modelu polimorficznego

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

  • Nie jest typowy dla interfejsu API REST, który jest przeznaczony do współdziałania ze wszystkimi językami.
  • Utrudnia uzasadnienie powiązanych modeli.

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

Zalecenia i najlepsze rozwiązania

Niestandardowe bindery 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 powinna obsłużyć błąd.
  • Są najbardziej przydatne do eliminowania powtarzających się kodów i kierunkowych problemów z metod akcji.
  • Zazwyczaj nie należy używać do konwertowania ciągu na typ niestandardowy, a zazwyczaj TypeConverter jest lepszą opcją.

Steve Smith

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

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

Ograniczenia domyślnego powiązania modelu

Domyślne powiązania modeli obsługują większość typowych typów danych .NET Core i powinny spełniać potrzeby większości deweloperów. Oczekują powiązania tekstowych danych wejściowych z żądania bezpośrednio z typami modeli. Może być konieczne przekształcenie danych wejściowych przed ich powiązaniem. Na przykład jeśli masz klucz, który może służyć do wyszukiwania danych modelu. Możesz użyć niestandardowego narzędzia do 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. Typ prosty jest konwertowany z pojedynczego ciągu w danych wejściowych. Typ złożony jest konwertowany z wielu wartości wejściowych. Framework określa różnicę na podstawie istnienia 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 przejrzeć sposób implementowania istniejących binderów modelu. Rozważmy wartość , która może służyć do konwertowania ciągów zakodowanych w formacie ByteArrayModelBinder base64 na tablice bajtowe. Tablice bajtów są często przechowywane jako pliki lub pola obiektów blob bazy danych.

Praca z 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 przekonwertować go na tablicę bajtów za pomocą klasy ByteArrayModelBinder . Argumenty ByteArrayModelBinderProvider byte[] mapują 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 bindera modelu można zaimplementować własny typ lub IModelBinderProvider użyć . ModelBinderAttribute

W poniższym przykładzie pokazano, jak przekonwertować ciąg zakodowany w formacie base64 na ciąg i zapisać ByteArrayModelBinder byte[] 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);
}

Możesz wysłać ciąg zakodowany w formacie base64 do tej metody interfejsu API przy użyciu narzędzia takiego jak Postman:

Listonosz

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ć ByteArrayModelBinder z 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 programu binder modelu

W tej sekcji zaim implementowany jest niestandardowy binder modelu, który:

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

W poniższym przykładzie użyto ModelBinder atrybutu 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 atrybut określa typ , który powinien być używany do powiązania ModelBinder IModelBinder parametrów Author akcji.

Następująca AuthorEntityBinder klasa wiąże parametr przez pobranie jednostki ze źródła danych przy użyciu Entity Framework Core Author i 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

Poprzednia klasa AuthorEntityBinder ma na celu zilustrowanie niestandardowego bindera modelu. Klasa nie ma na celu zilustrowania najlepszych rozwiązań dla scenariusza wyszukiwania. W przypadku wyszukiwania powiąż authorId element i odpytaj bazę danych w metodzie akcji. Takie podejście oddziela błędy powiązań modelu od NotFound przypadków.

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

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

Atrybutu ModelBinder można użyć do zastosowania do AuthorEntityBinder parametrów, które nie korzystają z domyślnych konwencji:

[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 domyślną wartością , jest ona określana dla authorId parametru przy użyciu ModelBinder atrybutu . Zarówno kontroler, jak i metoda akcji są uproszczone w porównaniu do szukania jednostki w metodzie akcji. Logika pobierania autora przy użyciu Entity Framework Core jest przenoszony do bindera modelu. Może to być znaczne uproszczenie, jeśli z modelem wiąże się kilka Author metod.

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

Implementowanie modeluBinderProvider

Zamiast stosować atrybut, można zaimplementować IModelBinderProvider . W ten sposób implementowane są wbudowane struktury binderów. Po określeniu typu, na którym działa binder, należy określić typ argumentu, który generuje, a nie dane wejściowe akceptowane przez binder. Następujący dostawca binderów współpracuje z programem AuthorEntityBinder . Po dodaniu go do kolekcji dostawców MVC nie trzeba używać atrybutu na parametrach ModelBinder Author lub Author typach.

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: poprzedni kod zwraca wartość BinderTypeModelBinder . BinderTypeModelBinder działa jako fabryka dla binderów modelu i zapewnia wstrzykiwanie zależności (DI). Wymaga AuthorEntityBinder di dostępu do EF Core. Użyj BinderTypeModelBinder , jeśli twój binder modelu wymaga usług z di.

Aby użyć niestandardowego dostawcy binderów modelu, dodaj go w 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 oceny elementów binderów modelu kolekcja dostawców jest analizowana w kolejności. Pierwszy dostawca, który zwraca binder, jest używany. Dodanie dostawcy na końcu kolekcji może spowodować wywoływanie wbudowanego bindera 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 używany dla Author argumentów akcji.

Powiązanie modelu polimorficznego

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

  • Nie jest typowy dla interfejsu API REST, który jest przeznaczony do współdziałania ze wszystkimi językami.
  • Utrudnia uzasadnienie powiązanych modeli.

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

Zalecenia i najlepsze rozwiązania

Niestandardowe bindery 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 powinna obsłużyć błąd.
  • Są najbardziej przydatne do eliminowania powtarzających się kodów i kierunkowych problemów z metod akcji.
  • Zazwyczaj nie należy używać do konwertowania ciągu na typ niestandardowy, a TypeConverter jest to zazwyczaj lepsza opcja.