4

I have a DTO class that is bound from body source to create my user:

    public class UserDto
    {
        [Required()]
        [MinLength(2)]
        [MaxLength(30)]
        public string FirstName { get; set; }

        [Required()]
        [MinLength(2)]
        [MaxLength(30)]
        public string LastName { get; set; }

        [Required]
        [SocialSerialNumber]
        public string SSN { get; set; }

        [Required]
        [PhoneNumber]
        public string PhoneNumber { get; set; }


        [Required]
        public bool? Gender { get; set; }  
        [Required]
        [MinLength(6)]
        [MaxLength(30)]
        [IgnoreTrim]  // this is what I need
        public string Password { get; set; }
    }

I want to trim (remove extra spaces from) all my strings in all of my models before validation. Trimming must be ignored for strings I explicitly specified as no-trim (maybe using an attribute called [IgnoreTrim]).

In the example above trimming for properties FirstName, LastName, PhoneNumber is needed but not Password.

I know that I can manually trim them in the controller action and then revalidate the model, But I'm looking for graceful way of doing this so that my action stays clean.

Mohammad Barbast
  • 1,753
  • 3
  • 17
  • 28

4 Answers4

5

I Found a nice solution using custom model binder and custom JSON converter.

public class StringTrimmerBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext is null)
            throw new ArgumentNullException(nameof(bindingContext));

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

        if (valueProviderResult == ValueProviderResult.None)
            return Task.CompletedTask;

        bindingContext.Result = ModelBindingResult.Success(valueProviderResult.FirstValue.Trim());
        return Task.CompletedTask;
    }
}

StringTrimmerBinder is my custom binder that transforms the string to a trimmed string using Trim() method. To use this binder I made a custom IModelBinderProvider that is is used to to resolve a binder for specified type (in my case string type).

public class CustomModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context is null)
            throw new ArgumentNullException(nameof(context));


        if (context.Metadata.ModelType == typeof(string) && context.BindingInfo.BindingSource!=BindingSource.Body)
            return new StringTrimmerBinder();

        return null;
    }
}

So I register CustomModelBinderProvider like this:

services.AddControllers(opt =>
{
    //registers CustomModelBinderProvider in the first place so that it is asked before other model binder providers.
    opt.ModelBinderProviders.Insert(0, new CustomModelBinderProvider());
});

Until now everything is ok and all string models are trimmed except those which their binding source is request body ([FromBody]). Because default model binders uses System.Text.Json namespace to convert request body to model types, instead of making a new binder that to the same job, I create a JSON converter that customize converting body to model types.

Here is my custom converter:

public class StringTrimmerJsonConverter : JsonConverter<string>
{
    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return reader.GetString().Trim();
    }

    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value);
    }
}

And here is the way to use this converter:

services.AddControllers(opt =>
{
    opt.ModelBinderProviders.Insert(0, new CustomModelBinderProvider());
})
    .AddJsonOptions(opt =>
    {
        opt.JsonSerializerOptions.Converters.Add(new StringTrimmerJsonConverter());
    });

That's it, now all my strings whether in complex-type or in simple-type will be trimmed.

Mohammad Barbast
  • 1,753
  • 3
  • 17
  • 28
2

Update

I have included two model binders that work with JSON and Form fields data from the body.

Implement IModelBinder and do the following:

Bind from Json:

public class CustomModelBinderJson : IModelBinder
{
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var request = bindingContext.HttpContext.Request;
        string body;
        using (StreamReader sr = new StreamReader(request.Body, Encoding.UTF8))
        {
            body = await sr.ReadToEndAsync();
        }

        var dataModel = JsonSerializer.Deserialize<DataModel>(body);

        dataModel.Name = dataModel.Name.Trim();

        bindingContext.Result = ModelBindingResult.Success(dataModel);
    }
}

Or bind from form fields:

public class CustomModelBinderForm : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var request = bindingContext.HttpContext.Request;
        var form = request.Form["Name"];
        var id = request.Form["Id"];
        var dataModel = new DataModel();

        dataModel.Id = int.Parse(id);
        dataModel.Name = form.ToString().Trim();

        bindingContext.Result = ModelBindingResult.Success(dataModel);
        return Task.CompletedTask;
    }
}

Then

 [ModelBinder(BinderType = typeof(CustomModelBinderForm))] //Or use the CustomModelBinderJson
    public class DataModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

Your action should look someting like this:

[HttpPost]
public IActionResult Index([FromBody] DataModel dataModel)
{
    //Do stuff
}

See MSDocs

HMZ
  • 2,949
  • 1
  • 19
  • 30
1

You may use the getter to trim the value that's going to be stored in a private field. In this way, your validation will be against the trimmed value

For example with the property FirstName

public class UserDto
{
    private string firstName;

    [Required()]
    [PersianChars]
    [MinLength(2)]
    [MaxLength(30)]
    public string FirstName 
    {
        get 
        {
            return firstName?.Trim(); 
        }
        set 
        {
            firstName = value;
        }
    }
}

Another solution is implementing a custom attribute that indicates which properties need to be trimmed before validation. At this point you can automate this process


To automate the process of trimming the values of the properties using a custom attribute:

You need to create a Custom Attribute
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/creating-custom-attributes

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class TrimmedValueAttribute : Attribute
{
}

Then, add the attribute to the desidered properties, for example FirstName

public class UserDto
{
    [Required()]
    [PersianChars]
    [MinLength(2)]
    [MaxLength(30)]
    [TrimmedValue]
    public string FirstName { get; set }
}

After that, you need to create an Action Filter which automatically trims the properties that has the TrimmedValueAttribute

https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-5.0#action-filters

public class TrimPropertiesActionFilter : IActionFilter 
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        Use reflection to find all the properties which has the TrimValueAttribute 
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
    }
}

Then, add the filter on your ConfigureServices method

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.Filters.Add(typeof(TrimPropertiesActionFilter));
    });
}

You can change the order of the action filters by settings the second argument of the Add method

options.Filters.Add(typeof(TrimPropertiesActionFilter, order: 0));
Manuel
  • 584
  • 2
  • 9
0

You can create a custom middleware and with a reflection find the string properties and trim them.

In case you want to trim specific string properties, you may implement a custom interface that includes the properties you wish to trim, and then in the custom middleware run over all the properties of the custom interface and trim them.

You can see an example of custom middleware here.

And also see how to trim a string with a reflection here.

And another approach would be to build a custom model binding to manipulate the data.

Misha Zaslavsky
  • 8,414
  • 11
  • 70
  • 116