Powiązanie modelu niestandardowego w ASP.NET Core

Autor: 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ą funkcję 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. Oczekuje się powiązania danych wejściowych opartych na tekście z żądania bezpośrednio z typami modelu. Może być konieczne przekształcenie danych wejściowych przed jego powiązaniem. Jeśli na przykład masz klucz, który może służyć do wyszukiwania danych modelu. Do pobierania danych na podstawie klucza można użyć niestandardowego powiązania modelu.

Tworzenie prostych i złożonych typów 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 przy użyciu TypeConverter metody lub TryParse metody. Typ złożony jest konwertowany z wielu wartości wejściowych. Struktura określa różnicę na podstawie istnienia obiektu TypeConverter lub TryParse. Zalecamy utworzenie konwertera typów lub użycie TryParse metody do stringSomeType konwersji, która nie wymaga zasobów zewnętrznych ani wielu danych wejściowych.

Zobacz Proste typy, aby uzyskać listę typów , które binder modelu może konwertować z ciągów.

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

Praca z aplikacją 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 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 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, poinformuj nas o tym w tym problemie z dyskusją w usłudze GitHub.

Ciąg zakodowany w formacie base64 można utworzyć w poprzedniej metodzie interfejsu API przy użyciu narzędzia takiego jak curl.

Jeśli binder może powiązać dane żądania 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 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 ModelBinder atrybut określa typ IModelBinder , 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 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 ma na celu zilustrowania najlepszych rozwiązań dla scenariusza wyszukiwania. Aby wyszukać, 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/{author}")]
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 z wyszukiwaniem jednostki w metodzie akcji. Logika pobierania autora przy użyciu programu 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 (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 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 argumentu, który generuje, 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 Authorlub Author -typed.

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 AuthorEntityBinder , aby di uzyskać dostęp do EF Core. Użyj BinderTypeModelBinder , jeśli powiązanie 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 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,
            };
        }
    }
}

Zalecenia 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łużyć błąd.
  • Jest to najbardziej przydatne w przypadku eliminowania powtarzających się problemów związanych z kodem i przecięciem ze strony metod akcji.
  • Zazwyczaj nie należy używać do konwertowania ciągu na typ niestandardowy, a TypeConverter zazwyczaj jest to lepsza opcja.

Autor: 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ą funkcję 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. Oczekuje się powiązania danych wejściowych opartych na tekście z żądania bezpośrednio z typami modelu. Może być konieczne przekształcenie danych wejściowych przed jego powiązaniem. Jeśli na przykład masz klucz, który może służyć do wyszukiwania danych modelu. Do pobierania danych na podstawie klucza można użyć niestandardowego powiązania modelu.

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 pojedynczego 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. Rozważ użycie ByteArrayModelBinder funkcji , która może służyć do konwertowania ciągów zakodowanych w formacie base64 na tablice bajtowe. Tablice bajtów są często przechowywane jako pliki lub pola obiektu BLOB bazy danych.

Praca z aplikacją 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[]))
    {
        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 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);
}

Ciąg zakodowany w formacie base64 można utworzyć w poprzedniej metodzie interfejsu API przy użyciu narzędzia takiego jak curl.

Jeśli binder może powiązać dane żądania 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 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 ModelBinder atrybut określa typ IModelBinder , 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

Poprzednia AuthorEntityBinder klasa ma na celu zilustrowanie niestandardowego powiązania modelu. Klasa nie ma na celu zilustrowania najlepszych rozwiązań dla scenariusza wyszukiwania. Aby wyszukać, 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/{author}")]
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 z wyszukiwaniem jednostki w metodzie akcji. Logika pobierania autora przy użyciu programu 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 (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 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 argumentu, który generuje, 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 Authorlub Author -typed.

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 AuthorEntityBinder , aby di uzyskać dostęp do EF Core. Użyj BinderTypeModelBinder , jeśli powiązanie 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 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,
            };
        }
    }
}

Zalecenia 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łużyć błąd.
  • Jest to najbardziej przydatne w przypadku eliminowania powtarzających się problemów związanych z kodem i przecięciem ze strony metod akcji.
  • Zazwyczaj nie należy używać do konwertowania ciągu na typ niestandardowy, a TypeConverter zazwyczaj jest to lepsza opcja.