Search code examples
asp.net-coremodel-bindingasp.net-core-5.0

How to normalize string inputs with bindings in ASP.NET Core 5


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?


Solution

  • 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.

    enter image description here

    Get it in StringModelBinder.

    enter image description here