Dela via


Bindning av anpassad modell i ASP.NET Core

Av Kirk Larkin

Med modellbindning kan kontrollantåtgärder fungera direkt med modelltyper (skickas som metodargument) i stället för HTTP-begäranden. Mappning mellan inkommande begärandedata och programmodeller hanteras av modellbindare. Utvecklare kan utöka de inbyggda funktionerna för modellbindning genom att implementera anpassade modellbindningar (men vanligtvis behöver du inte skriva en egen leverantör).

Visa eller ladda ned exempelkod (hur du laddar ned)

Begränsningar i standardmodellbindaren

Standardmodellens pärmar stöder de flesta vanliga .NET Core-datatyper och bör uppfylla de flesta utvecklares behov. De förväntar sig att binda textbaserade indata från begäran direkt till modelltyper. Du kan behöva transformera indata innan du binder dem. När du till exempel har en nyckel som kan användas för att söka efter modelldata. Du kan använda en anpassad modellbindning för att hämta data baserat på nyckeln.

Modellbindning av enkla och komplexa typer

Modellbindning använder specifika definitioner för de typer som den fungerar på. En enkel typ konverteras från en enda sträng med hjälp av TypeConverter eller en TryParse metod. En komplex typ konverteras från flera indatavärden. Ramverket avgör skillnaden baserat på förekomsten av en TypeConverter eller TryParse. Vi rekommenderar att du skapar en typkonverterare eller använder TryParse för en string till-konvertering SomeType som inte kräver externa resurser eller flera indata.

Se Enkla typer för en lista över typer som modellbindaren kan konvertera från strängar.

Innan du skapar en egen anpassad modellbindning är det värt att granska hur befintliga modellbindare implementeras. Tänk på vilka ByteArrayModelBinder som kan användas för att konvertera base64-kodade strängar till bytematriser. Bytematriserna lagras ofta som filer eller databasblobfält.

Arbeta med ByteArrayModelBinder

Base64-kodade strängar kan användas för att representera binära data. En bild kan till exempel kodas som en sträng. Exemplet innehåller en bild som en base64-kodad sträng i Base64String.txt.

ASP.NET Core MVC kan ta en base64-kodad sträng och använda en ByteArrayModelBinder för att konvertera den till en bytematris. Mappar ByteArrayModelBinderProvider argumenten byte[] till 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;
}

När du skapar en egen anpassad modellbindning kan du implementera din egen IModelBinderProvider typ eller använda ModelBinderAttribute.

I följande exempel visas hur du använder ByteArrayModelBinder för att konvertera en base64-kodad sträng till en byte[] och spara resultatet i en fil:

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

Om du vill se kodkommentar översatta till andra språk än engelska kan du meddela oss i det här GitHub-diskussionsproblemet.

Du kan POSTa en base64-kodad sträng till den tidigare API-metoden med hjälp av ett verktyg som curl.

Så länge bindemedlet kan binda begärandedata till korrekt namngivna egenskaper eller argument lyckas modellbindningen. I följande exempel visas hur du använder ByteArrayModelBinder med en vymodell:

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

Exempel på anpassad modellbindning

I det här avsnittet implementerar vi en anpassad modellbindning som:

  • Konverterar inkommande begärandedata till starkt typerade nyckelargument.
  • Använder Entity Framework Core för att hämta den associerade entiteten.
  • Skickar den associerade entiteten som ett argument till åtgärdsmetoden.

Följande exempel använder ModelBinder attributet för Author modellen:

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

I föregående kod ModelBinder anger attributet vilken typ av IModelBinder som ska användas för att binda Author åtgärdsparametrar.

Följande AuthorEntityBinder klass binder en Author parameter genom att hämta entiteten från en datakälla med Entity Framework Core och en 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;
    }
}

Note

Den föregående AuthorEntityBinder klassen är avsedd att illustrera en anpassad modellbindare. Klassen är inte avsedd att illustrera metodtips för ett uppslagsscenario. För att göra en sökning, bind authorId och kör en databasfråga i en funktionsmetod. Den här metoden skiljer modellbindningsfel från NotFound fall.

Följande kod visar hur du använder AuthorEntityBinder i en åtgärdsmetod:

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

    return Ok(author);
}

Attributet ModelBinder kan användas för att tillämpa på AuthorEntityBinder parametrar som inte använder standardkonventioner:

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

    return Ok(author);
}

I det här exemplet, eftersom namnet på argumentet inte är standard authorId, anges det på parametern med hjälp av ModelBinder attributet. Både kontrollanten och åtgärdsmetoden förenklas jämfört med att leta upp entiteten i åtgärdsmetoden. Logiken för att hämta författaren med Entity Framework Core flyttas till modellbindningen. Detta kan vara en betydande förenkling när du har flera metoder som binder till Author modellen.

Du kan använda ModelBinder attributet för enskilda modellegenskaper (till exempel på en viewmodel) eller för åtgärdsmetodparametrar för att ange ett visst modellbindnings- eller modellnamn för just den typen eller åtgärden.

Implementera en ModelBinderProvider

I stället för att använda ett attribut kan du implementera IModelBinderProvider. Så här implementeras de inbyggda ramverksbindarna. När du anger vilken typ av bindemedel som ska användas, anger du vilken typ av argument det genererar, inte vilken indata bindemedlet acceptera. Följande pärmleverantör samarbetar med AuthorEntityBinder. När det läggs till i MVC:s uppsättning providers behöver du inte använda ModelBinder-attributen på Author- eller Author-typade parametrar.

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

Obs! Föregående kod returnerar en BinderTypeModelBinder. BinderTypeModelBinder fungerar som en fabrik för modellbindare och tillhandahåller beroendeinjektion (DI). AuthorEntityBinder kräver DI för åtkomst till EF Core. Använd BinderTypeModelBinder om modellbindaren kräver tjänster från DI.

Om du vill använda en anpassad modellbindningsprovider lägger du till den i ConfigureServices:

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

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

Vid utvärdering av modellbindningar granskas samlingen av leverantörer i ordning. Den första providern som returnerar en binder som matchar indatamodellen används. Om du lägger till leverantören i slutet av samlingen kan det leda till att en inbyggd modellbindare anropas innan din anpassade bindare har en chans. I det här exemplet läggs den anpassade providern till i början av samlingen för att säkerställa att den alltid används för Author åtgärdsargument.

Polymorf modellbindning

Bindning till olika modeller av härledda typer kallas polymorf modellbindning. Polymorfisk anpassad modellbindning krävs när begärandevärdet måste bindas till den specifika härledda modelltypen. Polymorf modellbindning:

  • Är inte typiskt för ett REST API som är utformat för att samverka med alla språk.
  • Gör det svårt att resonera om de bundna modellerna.

Men om en app kräver polymorf modellbindning kan en implementering se ut som följande kod:

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

Rekommendationer och metodtips

Anpassade modellbindare:

  • Bör inte försöka ange statuskoder eller returnera resultat (till exempel 404 Hittades inte). Om modellbindningen misslyckas bör ett åtgärdsfilter eller logik i själva åtgärdsmetoden hantera felet.
  • Är mest användbara för att eliminera repetitiv kod och övergripande problem från åtgärdsmetoder.
  • Normalt bör inte användas för att konvertera en sträng till en anpassad typ, en TypeConverter är vanligtvis ett bättre alternativ.

Av Steve Smith

Med modellbindning kan kontrollantåtgärder fungera direkt med modelltyper (skickas som metodargument) i stället för HTTP-begäranden. Mappning mellan inkommande begärandedata och programmodeller hanteras av modellbindare. Utvecklare kan utöka de inbyggda funktionerna för modellbindning genom att implementera anpassade modellbindningar (men vanligtvis behöver du inte skriva en egen leverantör).

Visa eller ladda ned exempelkod (hur du laddar ned)

Begränsningar i standardmodellbindaren

Standardmodellens pärmar stöder de flesta vanliga .NET Core-datatyper och bör uppfylla de flesta utvecklares behov. De förväntar sig att binda textbaserade indata från begäran direkt till modelltyper. Du kan behöva transformera indata innan du binder dem. När du till exempel har en nyckel som kan användas för att söka efter modelldata. Du kan använda en anpassad modellbindning för att hämta data baserat på nyckeln.

Granskning av modellbindning

Modellbindning använder specifika definitioner för de typer som den fungerar på. En enkel datatyp konverteras från en enda sträng i ingången. En komplex typ konverteras från flera indatavärden. Ramverket avgör skillnaden baserat på förekomsten av en TypeConverter. Vi rekommenderar att du skapar en typkonverterare om du har en enkel string->SomeType-mappning som inte kräver externa resurser.

Innan du skapar en egen anpassad modellbindning är det värt att granska hur befintliga modellbindare implementeras. Tänk på vilka ByteArrayModelBinder som kan användas för att konvertera base64-kodade strängar till bytematriser. Bytematriserna lagras ofta som filer eller databasblobfält.

Arbeta med ByteArrayModelBinder

Base64-kodade strängar kan användas för att representera binära data. En bild kan till exempel kodas som en sträng. Exemplet innehåller en bild som en base64-kodad sträng i Base64String.txt.

ASP.NET Core MVC kan ta en base64-kodad sträng och använda en ByteArrayModelBinder för att konvertera den till en bytematris. Mappar ByteArrayModelBinderProvider argumenten byte[] till 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;
}

När du skapar en egen anpassad modellbindning kan du implementera din egen IModelBinderProvider typ eller använda ModelBinderAttribute.

I följande exempel visas hur du använder ByteArrayModelBinder för att konvertera en base64-kodad sträng till en byte[] och spara resultatet i en fil:

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

Du kan POSTa en base64-kodad sträng till den tidigare API-metoden med hjälp av ett verktyg som curl.

Så länge bindemedlet kan binda begärandedata till korrekt namngivna egenskaper eller argument lyckas modellbindningen. I följande exempel visas hur du använder ByteArrayModelBinder med en vymodell:

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

Exempel på anpassad modellbindning

I det här avsnittet implementerar vi en anpassad modellbindning som:

  • Konverterar inkommande begärandedata till starkt typerade nyckelargument.
  • Använder Entity Framework Core för att hämta den associerade entiteten.
  • Skickar den associerade entiteten som ett argument till åtgärdsmetoden.

Följande exempel använder ModelBinder attributet för Author modellen:

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

I föregående kod ModelBinder anger attributet vilken typ av IModelBinder som ska användas för att binda Author åtgärdsparametrar.

Följande AuthorEntityBinder klass binder en Author parameter genom att hämta entiteten från en datakälla med Entity Framework Core och en 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;
    }
}

Note

Den föregående AuthorEntityBinder klassen är avsedd att illustrera en anpassad modellbindare. Klassen är inte avsedd att illustrera metodtips för ett uppslagsscenario. För att göra en sökning, bind authorId och kör en databasfråga i en funktionsmetod. Den här metoden skiljer modellbindningsfel från NotFound fall.

Följande kod visar hur du använder AuthorEntityBinder i en åtgärdsmetod:

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

Attributet ModelBinder kan användas för att tillämpa på AuthorEntityBinder parametrar som inte använder standardkonventioner:

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

    return Ok(author);
}

I det här exemplet, eftersom namnet på argumentet inte är standard authorId, anges det på parametern med hjälp av ModelBinder attributet. Både kontrollanten och åtgärdsmetoden förenklas jämfört med att leta upp entiteten i åtgärdsmetoden. Logiken för att hämta författaren med Entity Framework Core flyttas till modellbindningen. Detta kan vara en betydande förenkling när du har flera metoder som binder till Author modellen.

Du kan använda ModelBinder attributet för enskilda modellegenskaper (till exempel på en viewmodel) eller för åtgärdsmetodparametrar för att ange ett visst modellbindnings- eller modellnamn för just den typen eller åtgärden.

Implementera en ModelBinderProvider

I stället för att använda ett attribut kan du implementera IModelBinderProvider. Så här implementeras de inbyggda ramverksbindarna. När du anger vilken typ av bindemedel som ska användas, anger du vilken typ av argument det genererar, inte vilken indata bindemedlet acceptera. Följande pärmleverantör samarbetar med AuthorEntityBinder. När det läggs till i MVC:s uppsättning providers behöver du inte använda ModelBinder-attributen på Author- eller Author-typade parametrar.

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

Obs! Föregående kod returnerar en BinderTypeModelBinder. BinderTypeModelBinder fungerar som en fabrik för modellbindare och tillhandahåller beroendeinjektion (DI). AuthorEntityBinder kräver DI för åtkomst till EF Core. Använd BinderTypeModelBinder om modellbindaren kräver tjänster från DI.

Om du vill använda en anpassad modellbindningsprovider lägger du till den i 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);
}

Vid utvärdering av modellbindningar granskas samlingen av leverantörer i ordning. Den första leverantören som returnerar en bindare används. Om du lägger till din provider i slutet av samlingen kan det leda till att en inbyggd modellbindare anropas innan din anpassade bindare får en chans. I det här exemplet läggs den anpassade providern till i början av samlingen för att säkerställa att den används för Author åtgärdsargument.

Polymorf modellbindning

Bindning till olika modeller av härledda typer kallas polymorf modellbindning. Polymorfisk anpassad modellbindning krävs när begärandevärdet måste bindas till den specifika härledda modelltypen. Polymorf modellbindning:

  • Är inte typiskt för ett REST API som är utformat för att samverka med alla språk.
  • Gör det svårt att resonera om de bundna modellerna.

Men om en app kräver polymorf modellbindning kan en implementering se ut som följande kod:

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

Rekommendationer och metodtips

Anpassade modellbindare:

  • Bör inte försöka ange statuskoder eller returnera resultat (till exempel 404 Hittades inte). Om modellbindningen misslyckas bör ett åtgärdsfilter eller logik i själva åtgärdsmetoden hantera felet.
  • Är mest användbara för att eliminera repetitiv kod och övergripande problem från åtgärdsmetoder.
  • Normalt bör inte användas för att konvertera en sträng till en anpassad typ, en TypeConverter är vanligtvis ett bättre alternativ.