I need to normalized some string data (replace some characters with each other like: 'ی' with 'ي' or trim it). To do so, I have created the following model binder like the following:
public class StringModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
return Task.CompletedTask;
var value = Normalize(valueProviderResult.FirstValue);
bindingContext.Result = ModelBindingResult.Success(value);
return Task.CompletedTask;
}
}
This binder works for both Query
and Route
bindings, but fails if I use FromBody
attribute. It fails because the BindModelAsync
method never gets called. I found another question raised for this issue here but sadly it does not have an answer for ASP.NET Core 5.0
I tried to extend the ComplexObjectModelBinder
but it is a sealed
class (and also does not provide any constructor). So I tried to extend ComplexTypeModelBinder
which is annotated as obsoleted.
I have copied the logic from ComplexTypeModelBinderProvider
from the source code and to my surprise, the BindModelAsync
of my StringModelBinder
receives calls now. But still fails because of the bindingContext.ValueProvider
contains only a provider for route and the result remains null.
My binder provider at this stage:
public class MyModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.IsComplexType && !context.Metadata.IsCollectionType)
{
var propertyBinders = new Dictionary<ModelMetadata, IModelBinder>();
for (var i = 0; i < context.Metadata.Properties.Count; i++)
{
var property = context.Metadata.Properties[i];
propertyBinders.Add(property, context.CreateBinder(property));
}
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
return new ComplexTypeModelBinder(
propertyBinders,
loggerFactory,
allowValidatingTopLevelNodes: true);
}
if (context.Metadata.ModelType == typeof(string))
{
return new StringModelBinder();
}
return null;
}
}
I also tried to create a provider from the body and changed my StringModelBinder
to:
public class StringModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
var context = new ValueProviderFactoryContext(bindingContext.ActionContext);
await new FormValueProviderFactory().CreateValueProviderAsync(context);
valueProviderResult = context.ValueProviders
.Select(x => x.GetValue(bindingContext.ModelName))
.FirstOrDefault(x => x != ValueProviderResult.None);
if (valueProviderResult == ValueProviderResult.None) return;
}
var value = valueProviderResult.FirstValue.Replace("A", "B");
bindingContext.Result = ModelBindingResult.Success(value);
}
}
How do I do this normalization in ASP.NET Core 5.0?
FromBody
is different from the FromQuery
and other HTTP verbs.
In the complex model binding (FromBody), you can get them in bindingContext.HttpContext.Request.Body
.
public class StringModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
{
var body = reader.ReadToEndAsync();
var mydata = body.Result;
//...
bindingContext.Result = ModelBindingResult.Success(mydata);
}
//...
}
}
action
[HttpPost]
public IActionResult test1([ModelBinder(binderType: typeof(StringModelBinder))]string model)
{
return Ok(model);
}
Then, pass a string into action.
Get it in StringModelBinder
.