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

Viewed 58429

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?

16 Answers

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.

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

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

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

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.

Related