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

17

190

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?

Arquit answered 11/11, 2009 at 22:25 Comment(0)
L
227
  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.

Langham answered 14/11, 2009 at 11:50 Comment(8)
i'd just replace the code in the inner most {} with this for brevity: string stringValue = (string)value; value = string.IsNullOrEmpty(stringValue) ? stringValue : stringValue.Trim();Due
This deserves more upvotes. I'm actually surprised the MVC team didn't choose to implement this in the default model binder...Seismo
This works well when I'm binding to a model that has properties, but what if I'm binding directly to strings (e.g. a controller that accepts multiple string parameters?) SetProperty isn't getting called. Do I need to override BindModel?Cranial
@BreckFresen I had the same problem, you will need to override the BindModel method and check the bindingContext.ModelType for a string then trim if it is.Oeflein
For anybody like me getting an ambiguity on DefaultModelBinder, the correct one is using System.Web.Mvc.Subdued
How would you modify this to leave type="password" inputs untouched?Metabolism
If your password fields have the correct datatype set (i.e. [DataType(DataType.Password)]) then you can do the following check to not trim these fields: bindingContext.ModelMetadata.DataTypeName != "Password"Gemmulation
Would be extremely useful to bake into aspnetcore. Maybe someone can re-open the ticket. github.com/dotnet/aspnetcore/issues/11837Komatik
L
80

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

Late answered 28/5, 2011 at 20:39 Comment(13)
+1 for a clean solution! You could improve the readability of your code even more by changing the order of the return statements and by negating the condition: if (valueResult == null || string.IsNullOrEmpty(valueResult.AttemptedValue)) return null;Lucialucian
This doesn't handle the [ValidateInput(false)] controller attribute. It causes the "Dangerous Request..." exception.Asquint
You must call "bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueResult)" otherwise the ModelState values are not available in the MVC controller (as they with the default binder) and the usual accept/reject pattern with ModelState.Clear no longer works as expected.Toile
@CodeChief Please can you give some examples of these situations that aren't handled by this implementation?Arcadia
Hi Ian, yes it's a simple test. Add or remove your string binder in the global route setup function, then test the end result by setting a breakpoint in any post-back action method. In the debugger examine the contents of the ModelState key values. You will notice they are normally there (with the default binder) but not for all string properties when you intercept them without a "set". See TryUpdateModel()/UpdateModel() and ModelState add error methods in MSDN. Okay if you never want to use the MVC accept/reject model behaviour, but not if you're making a standard component/full MVC support.Toile
Actually I don't like that behaviour, seems like Microsoft are trying to do something like what a DataSet provides for tabular data (previous/current/proposed values with accept/reject). A simpler (perhaps the most intuitive) way of operation would be that the binder always sets all values it can directly on the model (like the TryUpdateModel call), but deals with invalid operation/format exceptions as individual property/control validation errors, automatically. Until the input is valid I don't want the post-back anyway and when valid I want the model to represent "what is on the screen".Toile
For those who are getting 'Dangerous Request...' exception, refer to this article - blogs.taiga.nl/martijn/2011/09/29/…Aquatic
A coworker of mine implemented a variation of this that caused all sorts of issues: issues.umbraco.org/issue/U4-6665 I would recommend returning null and empty as appropriate rather than always preferring one over the other (in your case, you always return null even when the value is an empty string).Antananarivo
@NicholasWestby thanks for pointing this out. I changed the implementation to return null or empty depending on values passed.Late
This seems to break the [AllowHtml] attribute on model properties (along with the [ValidateInput(false)] as CodeGrue mentioned aboveRaine
Following @GurjeetSinghDB's article solved the issue aboveRaine
Guys, this sample is BAD. You loose ModelState[""]. Please check out Phil Haacks solution, cause the missing bindingContext.ModelState.Add(bindingContext.ModelName, modelState); in the sample above will get you sooner-later in trouble!Diathermic
@Late , The IModelBinder interface has only method BindModel with return type bool. Then how did you used with return type object here ?Witter
N
49

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.

Narcisanarcissism answered 30/5, 2011 at 19:38 Comment(4)
How can we implement something like this attribute when using the IModelBinder approach by @Korayem? In some applications, I use a different (third-party) model binder (e.g., S#arp Archeticture's). I'd like to write this up in a private DLL shared between projects, so it needs to be an IModelBinder approach.Suffice
@CarlBussema Here's a question about accessing Attributes from within an IModelBinder. stackoverflow.com/questions/6205176Parturient
I think it's a great addition but I would replace the .Cast<object>().Any(a => a.GetType() == typeof(NoTrimAttribute)) with .OfType<NoTrimAttribute>().Any(). Just a little bit cleaner.Voidable
I put my attributes in a shared assembly because, as with data annotations, such attributes have a usage scope wider than just MVC, e.g. business tier, clients. One other observation, the "DisplayFormatAttribute(ConvertEmptyStringToNull)" controls whether the trimmed string will be saved as null or an empty string. The default is true (null) which I like but in case you require empty strings in your database (hopefully not) you can set it false to get that. Anyway, this is all good stuff, hope MS extend their attributes to include trimming and padding and lots of other common stuff like that.Toile
V
23

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();
    }
}
Vickievicksburg answered 28/3, 2018 at 3:37 Comment(5)
Your solution works fine! Thanks. I tried the others solutions for .Net Core using IModelBinderProvider, it didn't work.Puny
Except in startup.cs, it can also be used in Model as [JsonConverter(typeof(TrimmingStringConverter))]. Btw. is there a reason behind using .Insert() instead .Add() ?Cheng
@Cheng I guess I just did .Insert() instead of .Add() to ensure it gets run before other Converters. Can't remember now.Vickievicksburg
What is the performance overhead of this over DefaultContractResolver?Nikkinikkie
I find that using this causes case sensitivity issues. An object like { "Person": { "Age": 23 }} will result in a null Person object, but { "person": { "age": 23 }} will result in a non-null object. This wasn't an issue before adding the call to AddJsonOptions and passing in the TrimmingStringConverter -- to get around this, I had to reserialize the request body using middleware - https://mcmap.net/q/136877/-asp-net-core-modify-substitute-request-bodyMitrewort
B
17

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();
    }
}
Buggs answered 17/2, 2016 at 17:40 Comment(6)
Again, see comments above. This example does not handle the skipValidation requirement of IUnvalidatedValueProvider.Propel
@adrian, The IModelBinder interface has only method BindModel with return type bool. Then how did you used with return type object here ?Witter
@MagendranV I'm not sure which interface you're looking at, but this answer is based on the IModelBinder in ASP.NET MVC 5, which returns an object: learn.microsoft.com/en-us/previous-versions/aspnet/…Buggs
@AaronHudon I have updated my answer to include an example to handle skipping validationBuggs
If your password fields have the correct datatype set (i.e. [DataType(DataType.Password)]) then you can update the last line as follows so that it does not trim these fields: return string.IsNullOrWhiteSpace(attemptedValue) || bindingContext.ModelMetadata.DataTypeName == "Password" ? attemptedValue : attemptedValue.Trim();Gemmulation
It should be noted the namespace in use here is System.Web.Mvc. There are actually 3 namespaces in which IModelBinder exists: System.Web.Mvc, System.Web.ModelBinding, and System.Web.Http.ModelBindingKettle
T
14

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.

Toile answered 3/12, 2013 at 15:24 Comment(0)
P
8

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());
 });
Pulley answered 22/8, 2015 at 16:5 Comment(1)
It didnt work for me too, all my fields are null nowPuny
L
8

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())
Lotty answered 30/11, 2017 at 9:56 Comment(3)
+1. Exactly what I was looking for. What is the purpose of the "binderToFind" code in the Registration function?Pina
I am just trying to put custom provider with the fallback of SimpleTypeModelBinderProvider by maintaining same index.Lotty
Whole description can be found here vikutech.blogspot.in/2018/02/…Lotty
D
7

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();
});
Depravity answered 12/12, 2019 at 23:1 Comment(1)
To find the existing factory index in a single pass: int formValueProviderFactoryIndex = options.ValueProviderFactories.TakeWhile(f => f is not FormValueProviderFactory).Count();Bilow
K
5

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()));
        })
    });
Kinross answered 3/8, 2014 at 1:39 Comment(1)
2 things: 1 - Cache your client objects (like $(this)), 2 - You can never rely on client inputs, but you can definitely rely on server code. So your answer is a completion to the server code answers :)Selfmastery
B
3

Update: This answer is out of date for recent versions of ASP.NET Core. Use Bassem's answer instead.


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.

Bilow answered 1/3, 2017 at 15:23 Comment(2)
I Don't know why, but it doesn't work for ASP.NET Core 1.1.1. All the properties of the model object I get in the controller action are null. The "SetProperty" method is nerver called.Welcher
Didnt work for me, the space at the beginning of my property is still there.Puny
P
3

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());
        ...
    }
Propel answered 18/11, 2017 at 6:22 Comment(0)
F
2

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.

Feudatory answered 2/10, 2012 at 15:42 Comment(0)
K
0

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.

Krutz answered 14/3, 2018 at 1:11 Comment(3)
You can mutate the model but you cannot mutate the model state. This means the application will work all right but the interface, which takes values from the model state and not from the model, will be inconsistent and confusing.Barbate
Isn't the model's data the same as the state?Krutz
No, it is not. In particular, the values for asp-for controls will be filled from raw values in the model state while the model logic operates on the properties of the model. So you see a value that is not trimmed but it behaves as if it were.Barbate
A
0

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();
}
Admonitory answered 2/10, 2019 at 13:0 Comment(0)
B
0

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.

Barbate answered 3/2, 2022 at 21:58 Comment(0)
G
0

I adapted @Kai G's answer for System.Text.Json:

 using System;
 using System.Text.Json;
 using System.Text.Json.Serialization;

public class TrimmedStringConverter : JsonConverter<string>
    {
        public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(string);

        public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return reader.GetString() is string value ? value.Trim() : null;
        }

        public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value);
        }
    }
Goon answered 24/10, 2022 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.