Normalize string inputs with Binding ASP.Net Core 5

Hamid Mayeli

I need to normalized the 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 binding but fails if I use FromBody attribute. It fails because BindModelAsync never gets called. I found another question raised for this issue here and sadly does not have an answer.

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

Now the question is, what is the best way to do this normalization in .Net 5?

To whom may concern: This question may seem duplication but I could not find anything related to .Net 5 and if there is a question answering to the ComplexTypeModelBinder issue, it won't be suitable for .Net 5 as it is obsoleted.

Karney.

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

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related