LP13
LP13

Reputation: 34109

Custom model binder not retrieving value from value-provider

I have a Custom model binder that will convert posted values to another model. Issue is bindingContext.ValueProvider.GetValue(modelName) returns none even if there are values posted from client.

Action Method

[HttpPost]
public ActionResult Update([DataSourceRequest] DataSourceRequest request, 
                           [Bind(Prefix = "models")] AnotherModel items)
{
    return Ok();
}

Target Model Class

[ModelBinder(BinderType = typeof(MyModelBinder))]
public class AnotherModel
{
    IEnumerable<Dictionary<string, object>> Items { get; set; }
}

Cutomer Model Binder

public class MyModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // ISSUE: valueProviderResult is always None
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }


        //here i will convert valueProviderResult to AnotherModel


        return Task.CompletedTask;
    }
}

Quick watch shows ValueProvider does have values

enter image description here

UPDATE1

Inside the Update action method when i can iterate through IFormCollection, The Request.Form has all the Key and Value pair. Not sure why model binder is not able to retrieve it.

foreach (var f in HttpContext.Request.Form)
{
    var key = f.Key;
    var v = f.Value;
}

Upvotes: 1

Views: 3083

Answers (2)

Joma
Joma

Reputation: 3859

My example

In my client I send a header in request, this header is Base64String(Json Serialized object)

Object -> Json -> Base64.

Headers can't be multiline. With base64 we get 1 line.

All of this are applicable to Body and other sources.

Header class

public class RequestHeader : IHeader
{
    [Required]
    public PlatformType Platform { get; set; } //Windows / Android / Linux / MacOS / iOS

    [Required]
    public ApplicationType ApplicationType { get; set; } 

    [Required(AllowEmptyStrings = false)]
    public string UserAgent { get; set; } = null!; 

    [Required(AllowEmptyStrings = false)]
    public string ClientName { get; set; } = null!; 

    [Required(AllowEmptyStrings = false)]
    public string ApplicationName { get; set; } = null!;

    [Required(AllowEmptyStrings = true)]
    public string Token { get; set; } = null!; 


    public string ToSerializedString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

IHeader Interface

public interface IHeader
{
}

Model Binder

public class HeaderParameterModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        StringValues headerValue = bindingContext.HttpContext.Request.Headers.Where(h =>
        {
            string guid = Guid.NewGuid().ToString();
            return h.Key.Equals(bindingContext.ModelName ?? guid) | 
                   h.Key.Equals(bindingContext.ModelType.Name ?? guid) | 
                   h.Key.Equals(bindingContext.ModelMetadata.ParameterName); 
        }).Select(h => h.Value).FirstOrDefault();
        if (headerValue.Any())
        {
            try
            {
                //Convert started
                bindingContext.Model = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(headerValue)), bindingContext.ModelType);
                bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
            }
            catch
            {
            }
        }
        return Task.CompletedTask;
    }
}

Model Binder Provider

We can work with any BindingSource.

  • Body
  • BindingSource Custom
  • BindingSource Form
  • BindingSource FormFile
  • BindingSource Header
  • BindingSource ModelBinding
  • BindingSource Path
  • BindingSource Query
  • BindingSource Services
  • BindingSource Special


public class ParametersModelBinderProvider : IModelBinderProvider
{
    private readonly IConfiguration configuration;

    public ParametersModelBinderProvider(IConfiguration configuration)
    {
        this.configuration = configuration;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType.GetInterfaces().Where(value => value.Name.Equals(nameof(ISecurityParameter))).Any() && BindingSource.Header.Equals(context.Metadata.BindingSource))
        {
            return new SecurityParameterModelBinder(configuration);
        }

        if (context.Metadata.ModelType.GetInterfaces().Where(value=>value.Name.Equals(nameof(IHeader))).Any() && BindingSource.Header.Equals(context.Metadata.BindingSource))
        {
            return new HeaderParameterModelBinder();
        }
        return null!;
    }
}

In Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0,new ParametersModelBinderProvider(configuration));
    });
}

Controller action

ExchangeResult is my result class.

[HttpGet(nameof(Exchange))]
public ActionResult<ExchangeResult> Exchange([FromHeader(Name = nameof(RequestHeader))] RequestHeader header)
{
    //RequestHeader previously was processed in modelbinder.
    //RequestHeader is null or object instance.  
    //Some instructions
}

Upvotes: 1

Jeremy Lakeman
Jeremy Lakeman

Reputation: 11120

If you examine the source code of MVC's CollectionModelBinder, you'd notice that values of the form "name[index]" will return ValueProviderResult.None and need to be handled separately.

It seems like you're trying to solve the wrong problem. I'd suggest binding to a standard collection class like Dictionary.

Either;

public ActionResult Update([DataSourceRequest] DataSourceRequest request, 
                           [Bind(Prefix = "models")] Dictionary<string, RecordTypeName> items)

Or;

public class AnotherModel : Dictionary<string, RecordTypeName> {}

If you don't know what type each dictionary value will have at compile time, that's where a custom binder would come in handy.

Upvotes: 0

Related Questions