Best way to trim strings after data entry. Should I create a custom model binder?

asp.netasp.net MvcModel Binders

asp.net Problem Overview


I'm using ASP.NET MVC and I'd like all user entered string fields to be trimmed before they're inserted into the database. And since I have many data entry forms, I'm looking for an elegant way to trim all strings instead of explicitly trimming every user supplied string value. I'm interested to know how and when people are trimming strings.

I thought about perhaps creating a custom model binder and trimming any string values there...that way, all my trimming logic is contained in one place. Is this a good approach? Are there any code samples that do this?

asp.net Solutions


Solution 1 - asp.net

  public class TrimModelBinder : DefaultModelBinder
  {
    protected override void SetProperty(ControllerContext controllerContext, 
      ModelBindingContext bindingContext, 
      System.ComponentModel.PropertyDescriptor propertyDescriptor, object value)
    {
      if (propertyDescriptor.PropertyType == typeof(string))
      {
        var stringValue = (string)value;
        if (!string.IsNullOrWhiteSpace(stringValue))
        {
          value = stringValue.Trim();
        }
        else
        {
          value = null;
        }
      }

      base.SetProperty(controllerContext, bindingContext, 
                          propertyDescriptor, value);
    }
  }

How about this code?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();

Set global.asax Application_Start event.

Solution 2 - asp.net

This is @takepara same resolution but as an IModelBinder instead of DefaultModelBinder so that adding the modelbinder in global.asax is through

ModelBinders.Binders.Add(typeof(string),new TrimModelBinder());

The class:

public class TrimModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext,
    ModelBindingContext bindingContext)
    {
        ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueResult== null || valueResult.AttemptedValue==null)
           return null;
        else if (valueResult.AttemptedValue == string.Empty)
           return string.Empty;
        return valueResult.AttemptedValue.Trim();
    }
}

based on @haacked post: http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

Solution 3 - asp.net

One improvement to @takepara answer.

Somewere in project:

public class NoTrimAttribute : Attribute { }

In TrimModelBinder class change

if (propertyDescriptor.PropertyType == typeof(string))

to

if (propertyDescriptor.PropertyType == typeof(string) && !propertyDescriptor.Attributes.Cast<object>().Any(a => a.GetType() == typeof(NoTrimAttribute)))

and you can mark properties to be excluded from trimming with [NoTrim] attribute.

Solution 4 - asp.net

In ASP.Net Core 2 this worked for me. I'm using the [FromBody] attribute in my controllers and JSON input. To override the string handling in the JSON deserialization I registered my own JsonConverter:

services.AddMvcCore()
    .AddJsonOptions(options =>
        {
            options.SerializerSettings.Converters.Insert(0, new TrimmingStringConverter());
        })

And this is the converter:

public class TrimmingStringConverter : JsonConverter
{
    public override bool CanRead => true;
    public override bool CanWrite => false;

    public override bool CanConvert(Type objectType) => objectType == typeof(string);

    public override object ReadJson(JsonReader reader, Type objectType,
        object existingValue, JsonSerializer serializer)
    {
        if (reader.Value is string value)
        {
            return value.Trim();
        }

        return reader.Value;
    }

    public override void WriteJson(JsonWriter writer, object value,
        JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Solution 5 - asp.net

With improvements in C# 6, you can now write a very compact model binder that will trim all string inputs:

public class TrimStringModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        var attemptedValue = value?.AttemptedValue;

        return string.IsNullOrWhiteSpace(attemptedValue) ? attemptedValue : attemptedValue.Trim();
    }
}

You need to include this line somewhere in Application_Start() in your Global.asax.cs file to use the model binder when binding strings:

ModelBinders.Binders.Add(typeof(string), new TrimStringModelBinder());

I find it is better to use a model binder like this, rather than overriding the default model binder, because then it will be used whenever you are binding a string, whether that's directly as a method argument or as a property on a model class. However, if you override the default model binder as other answers here suggest, that will only work when binding properties on models, not when you have a string as an argument to an action method

Edit: a commenter asked about dealing with the situation when a field should not be validated. My original answer was reduced to deal just with the question the OP had posed, but for those who are interested, you can deal with validation by using the following extended model binder:

public class TrimStringModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var shouldPerformRequestValidation = controllerContext.Controller.ValidateRequest && bindingContext.ModelMetadata.RequestValidationEnabled;
        var unvalidatedValueProvider = bindingContext.ValueProvider as IUnvalidatedValueProvider;

        var value = unvalidatedValueProvider == null ?
          bindingContext.ValueProvider.GetValue(bindingContext.ModelName) :
          unvalidatedValueProvider.GetValue(bindingContext.ModelName, !shouldPerformRequestValidation);

        var attemptedValue = value?.AttemptedValue;

        return string.IsNullOrWhiteSpace(attemptedValue) ? attemptedValue : attemptedValue.Trim();
    }
}

Solution 6 - asp.net

Another variant of @takepara's answer but with a different twist:

  1. I prefer the opt-in "StringTrim" attribute mechanism (rather than the opt-out "NoTrim" example of @Anton).

  2. An additional call to SetModelValue is required to ensure the ModelState is populated correctly and the default validation/accept/reject pattern can be used as normal, i.e. TryUpdateModel(model) to apply and ModelState.Clear() to accept all changes.

Put this in your entity/shared library:

/// <summary>
/// Denotes a data field that should be trimmed during binding, removing any spaces.
/// </summary>
/// <remarks>
/// <para>
/// Support for trimming is implmented in the model binder, as currently
/// Data Annotations provides no mechanism to coerce the value.
/// </para>
/// <para>
/// This attribute does not imply that empty strings should be converted to null.
/// When that is required you must additionally use the <see cref="System.ComponentModel.DataAnnotations.DisplayFormatAttribute.ConvertEmptyStringToNull"/>
/// option to control what happens to empty strings.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class StringTrimAttribute : Attribute
{
}

Then this in your MVC application/library:

/// <summary>
/// MVC model binder which trims string values decorated with the <see cref="StringTrimAttribute"/>.
/// </summary>
public class StringTrimModelBinder : IModelBinder
{
    /// <summary>
    /// Binds the model, applying trimming when required.
    /// </summary>
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // Get binding value (return null when not present)
        var propertyName = bindingContext.ModelName;
        var originalValueResult = bindingContext.ValueProvider.GetValue(propertyName);
        if (originalValueResult == null)
            return null;
        var boundValue = originalValueResult.AttemptedValue;

        // Trim when required
        if (!String.IsNullOrEmpty(boundValue))
        {
            // Check for trim attribute
            if (bindingContext.ModelMetadata.ContainerType != null)
            {
                var property = bindingContext.ModelMetadata.ContainerType.GetProperties()
                    .FirstOrDefault(propertyInfo => propertyInfo.Name == bindingContext.ModelMetadata.PropertyName);
                if (property != null && property.GetCustomAttributes(true)
                    .OfType<StringTrimAttribute>().Any())
                {
                    // Trim when attribute set
                    boundValue = boundValue.Trim();
                }
            }
        }

        // Register updated "attempted" value with the model state
        bindingContext.ModelState.SetModelValue(propertyName, new ValueProviderResult(
            originalValueResult.RawValue, boundValue, originalValueResult.Culture));

        // Return bound value
        return boundValue;
    }
}

If you don't set the property value in the binder, even when you don't want to change anything, you will block that property from ModelState altogether! This is because you are registered as binding all string types, so it appears (in my testing) that the default binder will not do it for you then.

Solution 7 - asp.net

Extra info for anyone searching how to do this in ASP.NET Core 1.0. Logic has changed quite a lot.

I wrote a blog post about how to do it, it explains things in bit more detailed

So ASP.NET Core 1.0 solution:

Model binder to do the actual trimming

public class TrimmingModelBinder : ComplexTypeModelBinder  
{
    public TrimmingModelBinder(IDictionary propertyBinders) : base(propertyBinders)
    {
    }

    protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
    {
        if(result.Model is string)
        {
            string resultStr = (result.Model as string).Trim();
            result = ModelBindingResult.Success(resultStr);
        }

        base.SetProperty(bindingContext, modelName, propertyMetadata, result);
    }
}

Also you need Model Binder Provider in the latest version, this tells that should this binder be used for this model

public class TrimmingModelBinderProvider : 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();
            foreach (var property in context.Metadata.Properties)
            {
                propertyBinders.Add(property, context.CreateBinder(property));
            }

            return new TrimmingModelBinder(propertyBinders);
        }

        return null;
    }
}

Then it has to be registered in Startup.cs

 services.AddMvc().AddMvcOptions(options => {  
       options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
 });

Solution 8 - asp.net

In case of MVC Core

Binder:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Threading.Tasks;
public class TrimmingModelBinder
    : IModelBinder
{
    private readonly IModelBinder FallbackBinder;

    public TrimmingModelBinder(IModelBinder fallbackBinder)
    {
        FallbackBinder = fallbackBinder ?? throw new ArgumentNullException(nameof(fallbackBinder));
    }

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

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (valueProviderResult != null &&
            valueProviderResult.FirstValue is string str &&
            !string.IsNullOrEmpty(str))
        {
            bindingContext.Result = ModelBindingResult.Success(str.Trim());
            return Task.CompletedTask;
        }

        return FallbackBinder.BindModelAsync(bindingContext);
    }
}

Provider:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

public class TrimmingModelBinderProvider
    : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (!context.Metadata.IsComplexType && context.Metadata.ModelType == typeof(string))
        {
            return new TrimmingModelBinder(new SimpleTypeModelBinder(context.Metadata.ModelType));
        }

        return null;
    }
}

Registration function:

    public static void AddStringTrimmingProvider(this MvcOptions option)
    {
        var binderToFind = option.ModelBinderProviders
            .FirstOrDefault(x => x.GetType() == typeof(SimpleTypeModelBinderProvider));

        if (binderToFind == null)
        {
            return;
        }

        var index = option.ModelBinderProviders.IndexOf(binderToFind);
        option.ModelBinderProviders.Insert(index, new TrimmingModelBinderProvider());
    }

Register:

service.AddMvc(option => option.AddStringTrimmingProvider())

Solution 9 - asp.net

I created value providers to trim the query string parameter values and the form values. This was tested with ASP.NET Core 3 and works perfectly.

public class TrimmedFormValueProvider
    : FormValueProvider
{
    public TrimmedFormValueProvider(IFormCollection values)
        : base(BindingSource.Form, values, CultureInfo.InvariantCulture)
    { }

    public override ValueProviderResult GetValue(string key)
    {
        ValueProviderResult baseResult = base.GetValue(key);
        string[] trimmedValues = baseResult.Values.Select(v => v?.Trim()).ToArray();
        return new ValueProviderResult(new StringValues(trimmedValues));
    }
}

public class TrimmedQueryStringValueProvider
    : QueryStringValueProvider
{
    public TrimmedQueryStringValueProvider(IQueryCollection values)
        : base(BindingSource.Query, values, CultureInfo.InvariantCulture)
    { }

    public override ValueProviderResult GetValue(string key)
    {
        ValueProviderResult baseResult = base.GetValue(key);
        string[] trimmedValues = baseResult.Values.Select(v => v?.Trim()).ToArray();
        return new ValueProviderResult(new StringValues(trimmedValues));
    }
}

public class TrimmedFormValueProviderFactory
    : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context.ActionContext.HttpContext.Request.HasFormContentType)
            context.ValueProviders.Add(new TrimmedFormValueProvider(context.ActionContext.HttpContext.Request.Form));
        return Task.CompletedTask;
    }
}

public class TrimmedQueryStringValueProviderFactory
    : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        context.ValueProviders.Add(new TrimmedQueryStringValueProvider(context.ActionContext.HttpContext.Request.Query));
        return Task.CompletedTask;
    }
}

Then register the value provider factories in the ConfigureServices() function in Startup.cs

services.AddControllersWithViews(options =>
{
    int formValueProviderFactoryIndex = options.ValueProviderFactories.IndexOf(options.ValueProviderFactories.OfType<FormValueProviderFactory>().Single());
    options.ValueProviderFactories[formValueProviderFactoryIndex] = new TrimmedFormValueProviderFactory();

    int queryStringValueProviderFactoryIndex = options.ValueProviderFactories.IndexOf(options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
    options.ValueProviderFactories[queryStringValueProviderFactoryIndex] = new TrimmedQueryStringValueProviderFactory();
});

Solution 10 - asp.net

While reading through the excellent answers and comments above, and becoming increasingly confused, I suddenly thought, hey, I wonder if there's a jQuery solution. So for others who, like me, find ModelBinders a bit bewildering, I offer the following jQuery snippet that trims the input fields before the form gets submitted.

    $('form').submit(function () {
        $(this).find('input:text').each(function () {
            $(this).val($.trim($(this).val()));
        })
    });

Solution 11 - asp.net

For ASP.NET Core, replace the ComplexTypeModelBinderProvider with a provider that trims strings.

In your startup code ConfigureServices method, add this:

services.AddMvc()
	.AddMvcOptions(s => {
		s.ModelBinderProviders[s.ModelBinderProviders.TakeWhile(p => !(p is ComplexTypeModelBinderProvider)).Count()] = new TrimmingModelBinderProvider();
	})

Define TrimmingModelBinderProvider like this:

/// <summary>
/// Used in place of <see cref="ComplexTypeModelBinderProvider"/> to trim beginning and ending whitespace from user input.
/// </summary>
class TrimmingModelBinderProvider : IModelBinderProvider
{
	class TrimmingModelBinder : ComplexTypeModelBinder
	{
		public TrimmingModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders) : base(propertyBinders) { }

		protected override void SetProperty(ModelBindingContext bindingContext, string modelName, ModelMetadata propertyMetadata, ModelBindingResult result)
		{
			var value = result.Model as string;
			if (value != null)
				result = ModelBindingResult.Success(value.Trim());
			base.SetProperty(bindingContext, modelName, propertyMetadata, result);
		}
	}

	public IModelBinder GetBinder(ModelBinderProviderContext 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));
			}
			return new TrimmingModelBinder(propertyBinders);
		}
		return null;
	}
}

The ugly part of this is the copy and paste of the GetBinder logic from ComplexTypeModelBinderProvider, but there doesn't seem to be any hook to let you avoid this.

Solution 12 - asp.net

Late to the party, but the following is a summary of adjustments required for MVC 5.2.3 if you are to handle the skipValidation requirement of the build-in value providers.

public class TrimStringModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // First check if request validation is required
        var shouldPerformRequestValidation = controllerContext.Controller.ValidateRequest && 
            bindingContext.ModelMetadata.RequestValidationEnabled;

        // determine if the value provider is IUnvalidatedValueProvider, if it is, pass in the 
        // flag to perform request validation (e.g. [AllowHtml] is set on the property)
        var unvalidatedProvider = bindingContext.ValueProvider as IUnvalidatedValueProvider;

        var valueProviderResult = unvalidatedProvider?.GetValue(bindingContext.ModelName, !shouldPerformRequestValidation) ??
            bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        return valueProviderResult?.AttemptedValue?.Trim();
    }
}

Global.asax

    protected void Application_Start()
    {
        ...
        ModelBinders.Binders.Add(typeof(string), new TrimStringModelBinder());
        ...
    }

Solution 13 - asp.net

I disagree with the solution. You should override GetPropertyValue because the data for SetProperty could also be filled by the ModelState. To catch the raw data from the input elements write this:

 public class CustomModelBinder : System.Web.Mvc.DefaultModelBinder
{
    protected override object GetPropertyValue(System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, System.Web.Mvc.IModelBinder propertyBinder)
    {
        object value = base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);

        string retval = value as string;

        return string.IsNullOrWhiteSpace(retval)
                   ? value
                   : retval.Trim();
    }

}

Filter by propertyDescriptor PropertyType if you are really only interested in string values but it should not matter because everything what comes in is basically a string.

Solution 14 - asp.net

There have been a lot of posts suggesting an attribute approach. Here is a package that already has a trim attribute and many others: Dado.ComponentModel.Mutations or NuGet

public partial class ApplicationUser
{
    [Trim, ToLower]
    public virtual string UserName { get; set; }
}

// Then to preform mutation
var user = new ApplicationUser() {
    UserName = "   M@X_speed.01! "
}

new MutationContext<ApplicationUser>(user).Mutate();

After the call to Mutate(), user.UserName will be mutated to m@x_speed.01!.

This example will trim whitespace and case the string to lowercase. It doesn't introduce validation, but the System.ComponentModel.Annotations can be used alongside Dado.ComponentModel.Mutations.

Solution 15 - asp.net

I posted this in another thread. In asp.net core 2, I went in a different direction. I used an action filter instead. In this case the developer can either set it globally or use as an attribute for the actions he/she wants to apply the string trimming. This code runs after the model binding has taken place, and it can update the values in the model object.

Here is my code, first create an action filter:

public class TrimInputStringsAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        foreach (var arg in context.ActionArguments)
        {
            if (arg.Value is string)
            {
                string val = arg.Value as string;
                if (!string.IsNullOrEmpty(val))
                {
                    context.ActionArguments[arg.Key] = val.Trim();
                }

                continue;
            }

            Type argType = arg.Value.GetType();
            if (!argType.IsClass)
            {
                continue;
            }

            TrimAllStringsInObject(arg.Value, argType);
        }
    }

    private void TrimAllStringsInObject(object arg, Type argType)
    {
        var stringProperties = argType.GetProperties()
                                      .Where(p => p.PropertyType == typeof(string));

        foreach (var stringProperty in stringProperties)
        {
            string currentValue = stringProperty.GetValue(arg, null) as string;
            if (!string.IsNullOrEmpty(currentValue))
            {
                stringProperty.SetValue(arg, currentValue.Trim(), null);
            }
        }
    }
}

To use it, either register as global filter or decorate your actions with the TrimInputStrings attribute.

[TrimInputStrings]
public IActionResult Register(RegisterViewModel registerModel)
{
    // Some business logic...
    return Ok();
}

Solution 16 - asp.net

OK, I have this thing and it kinda works:

class TrimmingModelBinder : IModelBinder
{
  public Task BindModelAsync (ModelBindingContext ctx)
  {
  if
  (
    ctx .ModelName is string name
    && ctx .ValueProvider .GetValue (name) .FirstValue is string v)
  ctx .ModelState .SetModelValue
  (
    name,
    new ValueProviderResult
    ((ctx .Result = ModelBindingResult .Success (v .Trim ())) .Model as string));
  return Task .CompletedTask; }}

class AutoTrimAttribute : ModelBinderAttribute
{
  public AutoTrimAttribute ()
  { this .BinderType = typeof (TrimmingModelBinder); }}

It is a shame that there is no standard feature for this though.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJohnny OshikaView Question on Stackoverflow
Solution 1 - asp.nettakeparaView Answer on Stackoverflow
Solution 2 - asp.netKorayemView Answer on Stackoverflow
Solution 3 - asp.netAntonView Answer on Stackoverflow
Solution 4 - asp.netKai GView Answer on Stackoverflow
Solution 5 - asp.netadrianView Answer on Stackoverflow
Solution 6 - asp.netTony WallView Answer on Stackoverflow
Solution 7 - asp.netTuukka LindroosView Answer on Stackoverflow
Solution 8 - asp.netVikash KumarView Answer on Stackoverflow
Solution 9 - asp.netBassemView Answer on Stackoverflow
Solution 10 - asp.netEric NelsonView Answer on Stackoverflow
Solution 11 - asp.netEdward BreyView Answer on Stackoverflow
Solution 12 - asp.netAaron HudonView Answer on Stackoverflow
Solution 13 - asp.netrudimenterView Answer on Stackoverflow
Solution 14 - asp.netroydukkeyView Answer on Stackoverflow
Solution 15 - asp.netMarcos de AguiarView Answer on Stackoverflow
Solution 16 - asp.netChristopher YeleightonView Answer on Stackoverflow