Niestandardowe powiązanie modelu w ASP.NET CoreCustom Model Binding in ASP.NET Core

Steve Kowalski i Kirka LarkinBy Steve Smith and Kirk Larkin

Powiązanie modelu umożliwia działanie kontrolera do pracy bezpośrednio z typami modeli (przekazane jako argumenty metod), a nie żądaniami HTTP.Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. Mapowanie między danymi żądań przychodzących a modelami aplikacji jest obsługiwane przez program Binders.Mapping between incoming request data and application models is handled by model binders. Deweloperzy mogą rozszerzać wbudowaną funkcję powiązania modelu, implementując niestandardowe powiązania modelu (zazwyczaj nie trzeba pisać własnego dostawcy).Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

Wyświetl lub pobierz przykładowy kod (jak pobrać)View or download sample code (how to download)

Domyślne ograniczenia spinacza modeluDefault model binder limitations

Domyślne powiązania modeli obsługują większość wspólnych typów danych .NET Core i powinny spełniać większość potrzeb deweloperów.The default model binders support most of the common .NET Core data types and should meet most developers' needs. Oczekują one powiązania danych tekstowych na podstawie żądania bezpośrednio z typami modeli.They expect to bind text-based input from the request directly to model types. Może być konieczne przekształcenie danych wejściowych przed ich powiązaniem.You might need to transform the input prior to binding it. Na przykład, jeśli masz klucz, którego można użyć do wyszukiwania danych modelu.For example, when you have a key that can be used to look up model data. Do pobierania danych opartych na kluczu można użyć spinacza modelu niestandardowego.You can use a custom model binder to fetch data based on the key.

Przegląd powiązań modeluModel binding review

Powiązanie modelu używa określonych definicji dla typów, w których działa.Model binding uses specific definitions for the types it operates on. Typ prosty jest konwertowany z pojedynczego ciągu w danych wejściowych.A simple type is converted from a single string in the input. Typ złożony jest konwertowany z wielu wartości wejściowych.A complex type is converted from multiple input values. Struktura określa różnice w zależności od istnienia TypeConverter .The framework determines the difference based on the existence of a TypeConverter. Zalecamy utworzenie konwertera typów, jeśli istnieje proste string -> SomeType Mapowanie, które nie wymaga zasobów zewnętrznych.We recommended you create a type converter if you have a simple string -> SomeType mapping that doesn't require external resources.

Przed utworzeniem własnego spinacza modelu niestandardowego warto przejrzeć sposób implementacji istniejących spinaczy modelu.Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. Rozważ, ByteArrayModelBinder który może być używany do konwertowania ciągów zakodowanych algorytmem Base64 na tablice bajtowe.Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. Tablice bajtowe są często przechowywane jako pliki lub pola obiektów BLOB bazy danych.The byte arrays are often stored as files or database BLOB fields.

Praca z ByteArrayModelBinderWorking with the ByteArrayModelBinder

Ciągi kodowane algorytmem Base64 mogą służyć do reprezentowania danych binarnych.Base64-encoded strings can be used to represent binary data. Na przykład obraz może być zakodowany jako ciąg.For example, an image can be encoded as a string. Przykład zawiera obraz jako ciąg zakodowany algorytmem Base64 w Base64String.txt.The sample includes an image as a base64-encoded string in Base64String.txt.

ASP.NET Core MVC może przyjmować ciąg zakodowany algorytmem Base64 i używać go, ByteArrayModelBinder Aby przekonwertować go na tablicę bajtów.ASP.NET Core MVC can take a base64-encoded string and use a ByteArrayModelBinder to convert it into a byte array. ByteArrayModelBinderProviderMapowania byte[] argumentów do ByteArrayModelBinder :The ByteArrayModelBinderProvider maps byte[] arguments to 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 spinacza modelu niestandardowego można zaimplementować własny IModelBinderProvider Typ lub użyć ModelBinderAttribute .When creating your own custom model binder, you can implement your own IModelBinderProvider type, or use the ModelBinderAttribute.

Poniższy przykład pokazuje, jak użyć ByteArrayModelBinder do przekonwertowania ciągu zakodowanego algorytmem Base64 na byte[] a i zapisać wynik do pliku:The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[] and save the result to a file:

[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, poinformuj nas o tym problemie z dyskusjąw witrynie GitHub.If you would like to see code comments translated to languages other than English, let us know in this GitHub discussion issue.

Można OPUBLIKOWAĆ ciąg zakodowany algorytmem Base64 w tej metodzie interfejsu API za pomocą narzędzia, takiego jak Poster:You can POST a base64-encoded string to this api method using a tool like Postman:

Postmanpostman

Tak długo, jak spinacz może powiązać dane żądania do odpowiednio nazwanych właściwości lub argumentów, powiązanie modelu zakończy się pomyślnie.As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. Poniższy przykład przedstawia sposób użycia ByteArrayModelBinder z modelem widoku:The following example shows how to use ByteArrayModelBinder with a view model:

[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ładowy spinacz modelu niestandardowegoCustom model binder sample

W tej sekcji zaimplementujemy niestandardowy spinacz modelu, który:In this section we'll implement a custom model binder that:

  • Konwertuje przychodzące dane żądania na argumenty klucza o jednoznacznie określonym typie.Converts incoming request data into strongly typed key arguments.
  • Używa Entity Framework Core, aby pobrać skojarzoną jednostkę.Uses Entity Framework Core to fetch the associated entity.
  • Przekazuje skojarzoną jednostkę jako argument do metody akcji.Passes the associated entity as an argument to the action method.

Poniższy przykład używa ModelBinder atrybutu w Author modelu:The following sample uses the ModelBinder attribute on the Author model:

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 powinien być używany do wiązania Author parametrów akcji.In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind Author action parameters.

Następująca AuthorEntityBinder Klasa wiąże Author parametr przez pobranie jednostki ze źródła danych przy użyciu Entity Framework Core i authorId :The following AuthorEntityBinder class binds an Author parameter by fetching the entity from a data source using Entity Framework Core and an 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 jest przeznaczona do zilustrowania niestandardowego spinacza modelu.The preceding AuthorEntityBinder class is intended to illustrate a custom model binder. Klasa nie jest przeznaczona do zilustrowania najlepszych rozwiązań dotyczących scenariusza wyszukiwania.The class isn't intended to illustrate best practices for a lookup scenario. W celu wyszukania należy powiązać authorId bazę danych i zbadać ją w metodzie akcji.For lookup, bind the authorId and query the database in an action method. To podejście oddziela błędy powiązań modelu z NotFound przypadków.This approach separates model binding failures from NotFound cases.

Poniższy kod pokazuje, jak używać AuthorEntityBinder w metodzie akcji:The following code shows how to use the AuthorEntityBinder in an action method:

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

    return Ok(author);
}

Ten ModelBinder atrybut może służyć do stosowania AuthorEntityBinder parametrów do, które nie używają Konwencji domyślnych:The ModelBinder attribute can be used to apply the AuthorEntityBinder to parameters that don't use default conventions:

[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ą, jest authorId określona w parametrze przy użyciu ModelBinder atrybutu.In this example, since the name of the argument isn't the default authorId, it's specified on the parameter using the ModelBinder attribute. Zarówno kontroler, jak i Metoda akcji są uproszczone w porównaniu z wyszukiwaniem jednostki w metodzie akcji.Both the controller and action method are simplified compared to looking up the entity in the action method. Logika pobierania autora przy użyciu Entity Framework Core jest przenoszona do spinacza modelu.The logic to fetch the author using Entity Framework Core is moved to the model binder. Może to być znaczące uproszczenie, gdy istnieje kilka metod, które są powiązane z Author modelem.This can be a considerable simplification when you have several methods that bind to the Author model.

Można zastosować ModelBinder atrybut do poszczególnych właściwości modelu (na przykład na ViewModel) lub do parametrów metody akcji, aby określić określony spinacz modelu lub nazwę modelu dla tylko tego typu lub akcji.You can apply the ModelBinder attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

Implementowanie elementu ModelBinderProviderImplementing a ModelBinderProvider

Zamiast stosować atrybut, można zaimplementować IModelBinderProvider .Instead of applying an attribute, you can implement IModelBinderProvider. Jest to sposób implementacji wbudowanych powiązań struktury.This is how the built-in framework binders are implemented. Po określeniu typu, na którym działa Twój spinacz, należy określić typ argumentu, który produkuje, a nie dane wejściowe zaakceptowane przez spinacz.When you specify the type your binder operates on, you specify the type of argument it produces, not the input your binder accepts. Następujący dostawca programu Binder współpracuje z AuthorEntityBinder .The following binder provider works with the AuthorEntityBinder. Po dodaniu do kolekcji dostawców MVC nie trzeba używać ModelBinder atrybutów z Author Author parametrami lub typem.When it's added to MVC's collection of providers, you don't need to use the ModelBinder attribute on Author or Author-typed parameters.

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 .Note: The preceding code returns a BinderTypeModelBinder. BinderTypeModelBinderdziała jako fabryka dla segregatorów modelu i zapewnia iniekcję zależności (DI).BinderTypeModelBinder acts as a factory for model binders and provides dependency injection (DI). AuthorEntityBinderProgram wymaga dostępu EF Core.The AuthorEntityBinder requires DI to access EF Core. Użyj, BinderTypeModelBinder Jeśli model spinacza wymaga usług z di.Use BinderTypeModelBinder if your model binder requires services from DI.

Aby użyć niestandardowego dostawcy segregatorów modelu, Dodaj go w ConfigureServices :To use a custom model binder provider, add it in 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 sprawdzana w podanej kolejności.When evaluating model binders, the collection of providers is examined in order. Używany jest pierwszy dostawca zwracający spinacz.The first provider that returns a binder is used. Dodanie swojego dostawcy do końca kolekcji może spowodować wywołanie wbudowanego spinacza modelu, zanim niestandardowy spinacz będzie miał szansę.Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. W tym przykładzie niestandardowy dostawca zostanie dodany do początku kolekcji, aby upewnić się, że jest on używany dla Author argumentów akcji.In this example, the custom provider is added to the beginning of the collection to ensure it's used for Author action arguments.

Powiązanie modelu polimorficznegoPolymorphic model binding

Powiązanie z różnymi modelami typów pochodnych jest znane jako powiązanie modelu polimorficzne.Binding to different models of derived types is known as polymorphic model binding. Powiązanie modelu niestandardowego polimorficzne jest wymagane, gdy wartość żądania musi być powiązana z konkretnym typem modelu pochodnego.Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. Powiązanie modelu polimorficznego:Polymorphic model binding:

  • Nie jest typowy dla interfejsu API REST, który jest przeznaczony do współpracy ze wszystkimi językami.Isn't typical for a REST API that's designed to interoperate with all languages.
  • Utrudniają powody dotyczące modeli powiązanych.Makes it difficult to reason about the bound models.

Jeśli jednak aplikacja wymaga powiązania modelu polimorficznego, implementacja może wyglądać podobnie do następującego kodu:However, if an app requires polymorphic model binding, an implementation might look like the following code:

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] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

Zalecenia i najlepsze rozwiązaniaRecommendations and best practices

Niestandardowe powiązania modelu:Custom model binders:

  • Nie należy próbować ustawiać kodów stanu ani zwracać wyników (na przykład nie znaleziono 404).Shouldn't attempt to set status codes or return results (for example, 404 Not Found). Jeśli powiązanie modelu nie powiedzie się, Filtr akcji lub logika w samej metodzie akcji powinien obsłużyć błąd.If model binding fails, an action filter or logic within the action method itself should handle the failure.
  • Są najbardziej przydatne do eliminowania powtarzających się kodu i krzyżowego obaw z metod akcji.Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
  • Zwykle nie należy używać do konwertowania ciągu na typ niestandardowy, a TypeConverter jest zazwyczaj lepszym rozwiązaniem.Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.

Przez Steve SmithBy Steve Smith

Powiązanie modelu umożliwia działanie kontrolera do pracy bezpośrednio z typami modeli (przekazane jako argumenty metod), a nie żądaniami HTTP.Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. Mapowanie między danymi żądań przychodzących a modelami aplikacji jest obsługiwane przez program Binders.Mapping between incoming request data and application models is handled by model binders. Deweloperzy mogą rozszerzać wbudowaną funkcję powiązania modelu, implementując niestandardowe powiązania modelu (zazwyczaj nie trzeba pisać własnego dostawcy).Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

Wyświetl lub pobierz przykładowy kod (jak pobrać)View or download sample code (how to download)

Domyślne ograniczenia spinacza modeluDefault model binder limitations

Domyślne powiązania modeli obsługują większość wspólnych typów danych .NET Core i powinny spełniać większość potrzeb deweloperów.The default model binders support most of the common .NET Core data types and should meet most developers' needs. Oczekują one powiązania danych tekstowych na podstawie żądania bezpośrednio z typami modeli.They expect to bind text-based input from the request directly to model types. Może być konieczne przekształcenie danych wejściowych przed ich powiązaniem.You might need to transform the input prior to binding it. Na przykład, jeśli masz klucz, którego można użyć do wyszukiwania danych modelu.For example, when you have a key that can be used to look up model data. Do pobierania danych opartych na kluczu można użyć spinacza modelu niestandardowego.You can use a custom model binder to fetch data based on the key.

Przegląd powiązań modeluModel binding review

Powiązanie modelu używa określonych definicji dla typów, w których działa.Model binding uses specific definitions for the types it operates on. Typ prosty jest konwertowany z pojedynczego ciągu w danych wejściowych.A simple type is converted from a single string in the input. Typ złożony jest konwertowany z wielu wartości wejściowych.A complex type is converted from multiple input values. Struktura określa różnice w zależności od istnienia TypeConverter .The framework determines the difference based on the existence of a TypeConverter. Zalecamy utworzenie konwertera typów, jeśli istnieje proste string -> SomeType Mapowanie, które nie wymaga zasobów zewnętrznych.We recommended you create a type converter if you have a simple string -> SomeType mapping that doesn't require external resources.

Przed utworzeniem własnego spinacza modelu niestandardowego warto przejrzeć sposób implementacji istniejących spinaczy modelu.Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. Rozważ, ByteArrayModelBinder który może być używany do konwertowania ciągów zakodowanych algorytmem Base64 na tablice bajtowe.Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. Tablice bajtowe są często przechowywane jako pliki lub pola obiektów BLOB bazy danych.The byte arrays are often stored as files or database BLOB fields.

Praca z ByteArrayModelBinderWorking with the ByteArrayModelBinder

Ciągi kodowane algorytmem Base64 mogą służyć do reprezentowania danych binarnych.Base64-encoded strings can be used to represent binary data. Na przykład obraz może być zakodowany jako ciąg.For example, an image can be encoded as a string. Przykład zawiera obraz jako ciąg zakodowany algorytmem Base64 w Base64String.txt.The sample includes an image as a base64-encoded string in Base64String.txt.

ASP.NET Core MVC może przyjmować ciąg zakodowany algorytmem Base64 i używać go, ByteArrayModelBinder Aby przekonwertować go na tablicę bajtów.ASP.NET Core MVC can take a base64-encoded string and use a ByteArrayModelBinder to convert it into a byte array. ByteArrayModelBinderProviderMapowania byte[] argumentów do ByteArrayModelBinder :The ByteArrayModelBinderProvider maps byte[] arguments to 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 spinacza modelu niestandardowego można zaimplementować własny IModelBinderProvider Typ lub użyć ModelBinderAttribute .When creating your own custom model binder, you can implement your own IModelBinderProvider type, or use the ModelBinderAttribute.

Poniższy przykład pokazuje, jak użyć ByteArrayModelBinder do przekonwertowania ciągu zakodowanego algorytmem Base64 na byte[] a i zapisać wynik do pliku:The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[] and save the result to a file:

[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żna OPUBLIKOWAĆ ciąg zakodowany algorytmem Base64 w tej metodzie interfejsu API za pomocą narzędzia, takiego jak Poster:You can POST a base64-encoded string to this api method using a tool like Postman:

Postmanpostman

Tak długo, jak spinacz może powiązać dane żądania do odpowiednio nazwanych właściwości lub argumentów, powiązanie modelu zakończy się pomyślnie.As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. Poniższy przykład przedstawia sposób użycia ByteArrayModelBinder z modelem widoku:The following example shows how to use ByteArrayModelBinder with a view model:

[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ładowy spinacz modelu niestandardowegoCustom model binder sample

W tej sekcji zaimplementujemy niestandardowy spinacz modelu, który:In this section we'll implement a custom model binder that:

  • Konwertuje przychodzące dane żądania na argumenty klucza o jednoznacznie określonym typie.Converts incoming request data into strongly typed key arguments.
  • Używa Entity Framework Core, aby pobrać skojarzoną jednostkę.Uses Entity Framework Core to fetch the associated entity.
  • Przekazuje skojarzoną jednostkę jako argument do metody akcji.Passes the associated entity as an argument to the action method.

Poniższy przykład używa ModelBinder atrybutu w Author modelu:The following sample uses the ModelBinder attribute on the Author model:

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 powinien być używany do wiązania Author parametrów akcji.In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind Author action parameters.

Następująca AuthorEntityBinder Klasa wiąże Author parametr przez pobranie jednostki ze źródła danych przy użyciu Entity Framework Core i authorId :The following AuthorEntityBinder class binds an Author parameter by fetching the entity from a data source using Entity Framework Core and an 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 jest przeznaczona do zilustrowania niestandardowego spinacza modelu.The preceding AuthorEntityBinder class is intended to illustrate a custom model binder. Klasa nie jest przeznaczona do zilustrowania najlepszych rozwiązań dotyczących scenariusza wyszukiwania.The class isn't intended to illustrate best practices for a lookup scenario. W celu wyszukania należy powiązać authorId bazę danych i zbadać ją w metodzie akcji.For lookup, bind the authorId and query the database in an action method. To podejście oddziela błędy powiązań modelu z NotFound przypadków.This approach separates model binding failures from NotFound cases.

Poniższy kod pokazuje, jak używać AuthorEntityBinder w metodzie akcji:The following code shows how to use the AuthorEntityBinder in an action method:

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

Ten ModelBinder atrybut może służyć do stosowania AuthorEntityBinder parametrów do, które nie używają Konwencji domyślnych:The ModelBinder attribute can be used to apply the AuthorEntityBinder to parameters that don't use default conventions:

[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ą, jest authorId określona w parametrze przy użyciu ModelBinder atrybutu.In this example, since the name of the argument isn't the default authorId, it's specified on the parameter using the ModelBinder attribute. Zarówno kontroler, jak i Metoda akcji są uproszczone w porównaniu z wyszukiwaniem jednostki w metodzie akcji.Both the controller and action method are simplified compared to looking up the entity in the action method. Logika pobierania autora przy użyciu Entity Framework Core jest przenoszona do spinacza modelu.The logic to fetch the author using Entity Framework Core is moved to the model binder. Może to być znaczące uproszczenie, gdy istnieje kilka metod, które są powiązane z Author modelem.This can be a considerable simplification when you have several methods that bind to the Author model.

Można zastosować ModelBinder atrybut do poszczególnych właściwości modelu (na przykład na ViewModel) lub do parametrów metody akcji, aby określić określony spinacz modelu lub nazwę modelu dla tylko tego typu lub akcji.You can apply the ModelBinder attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

Implementowanie elementu ModelBinderProviderImplementing a ModelBinderProvider

Zamiast stosować atrybut, można zaimplementować IModelBinderProvider .Instead of applying an attribute, you can implement IModelBinderProvider. Jest to sposób implementacji wbudowanych powiązań struktury.This is how the built-in framework binders are implemented. Po określeniu typu, na którym działa Twój spinacz, należy określić typ argumentu, który produkuje, a nie dane wejściowe zaakceptowane przez spinacz.When you specify the type your binder operates on, you specify the type of argument it produces, not the input your binder accepts. Następujący dostawca programu Binder współpracuje z AuthorEntityBinder .The following binder provider works with the AuthorEntityBinder. Po dodaniu do kolekcji dostawców MVC nie trzeba używać ModelBinder atrybutów z Author Author parametrami lub typem.When it's added to MVC's collection of providers, you don't need to use the ModelBinder attribute on Author or Author-typed parameters.

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 .Note: The preceding code returns a BinderTypeModelBinder. BinderTypeModelBinderdziała jako fabryka dla segregatorów modelu i zapewnia iniekcję zależności (DI).BinderTypeModelBinder acts as a factory for model binders and provides dependency injection (DI). AuthorEntityBinderProgram wymaga dostępu EF Core.The AuthorEntityBinder requires DI to access EF Core. Użyj, BinderTypeModelBinder Jeśli model spinacza wymaga usług z di.Use BinderTypeModelBinder if your model binder requires services from DI.

Aby użyć niestandardowego dostawcy segregatorów modelu, Dodaj go w ConfigureServices :To use a custom model binder provider, add it in 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 sprawdzana w podanej kolejności.When evaluating model binders, the collection of providers is examined in order. Używany jest pierwszy dostawca zwracający spinacz.The first provider that returns a binder is used. Dodanie swojego dostawcy do końca kolekcji może spowodować wywołanie wbudowanego spinacza modelu, zanim niestandardowy spinacz będzie miał szansę.Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. W tym przykładzie niestandardowy dostawca zostanie dodany do początku kolekcji, aby upewnić się, że jest on używany dla Author argumentów akcji.In this example, the custom provider is added to the beginning of the collection to ensure it's used for Author action arguments.

Powiązanie modelu polimorficznegoPolymorphic model binding

Powiązanie z różnymi modelami typów pochodnych jest znane jako powiązanie modelu polimorficzne.Binding to different models of derived types is known as polymorphic model binding. Powiązanie modelu niestandardowego polimorficzne jest wymagane, gdy wartość żądania musi być powiązana z konkretnym typem modelu pochodnego.Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. Powiązanie modelu polimorficznego:Polymorphic model binding:

  • Nie jest typowy dla interfejsu API REST, który jest przeznaczony do współpracy ze wszystkimi językami.Isn't typical for a REST API that's designed to interoperate with all languages.
  • Utrudniają powody dotyczące modeli powiązanych.Makes it difficult to reason about the bound models.

Jeśli jednak aplikacja wymaga powiązania modelu polimorficznego, implementacja może wyglądać podobnie do następującego kodu:However, if an app requires polymorphic model binding, an implementation might look like the following code:

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] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

Zalecenia i najlepsze rozwiązaniaRecommendations and best practices

Niestandardowe powiązania modelu:Custom model binders:

  • Nie należy próbować ustawiać kodów stanu ani zwracać wyników (na przykład nie znaleziono 404).Shouldn't attempt to set status codes or return results (for example, 404 Not Found). Jeśli powiązanie modelu nie powiedzie się, Filtr akcji lub logika w samej metodzie akcji powinien obsłużyć błąd.If model binding fails, an action filter or logic within the action method itself should handle the failure.
  • Są najbardziej przydatne do eliminowania powtarzających się kodu i krzyżowego obaw z metod akcji.Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
  • Zwykle nie należy używać do konwertowania ciągu na typ niestandardowy, a TypeConverter jest zazwyczaj lepszym rozwiązaniem.Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.