La mejor manera de recortar cadenas después de la entrada de datos. ¿Debo crear un cuaderno de modelo personalizado?

172

Estoy usando ASP.NET MVC y me gustaría que todos los campos de cadena ingresados ​​por el usuario se recorten antes de insertarlos en la base de datos. Y dado que tengo muchos formularios de entrada de datos, estoy buscando una forma elegante de recortar todas las cadenas en lugar de recortar explícitamente cada valor de cadena proporcionado por el usuario. Me interesa saber cómo y cuándo las personas recortan las cuerdas.

Pensé en crear un archivador de modelo personalizado y recortar cualquier valor de cadena allí ... de esa manera, toda mi lógica de recorte está contenida en un solo lugar. ¿Es este un buen enfoque? ¿Hay ejemplos de código que hagan esto?

Johnny Oshika
fuente

Respuestas:

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

¿Qué tal este código?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();

Establezca el evento global.asax Application_Start.

takepara
fuente
3
simplemente reemplazaría el código en el interior {} con esto por brevedad: string stringValue = (string) value; value = string.IsNullOrEmpty (stringValue)? stringValue: stringValue.Trim ();
Simon_Weaver
44
Esto merece más votos a favor. En realidad me sorprende que el equipo MVC no eligió para implementar esto en la carpeta por defecto del modelo ...
Portman
1
@BreckFresen Tuve el mismo problema, tendrá que anular el método BindModel y verificar el enlaceContext.ModelType para una cadena y luego recortar si es así.
Kelly
3
Para cualquiera como yo que tenga una ambigüedad en DefaultModelBinder, la correcta es usar System.Web.Mvc.
GeoffM
3
¿Cómo modificaría esto para dejar las type="password"entradas intactas?
Extragorey
77

Esta es @takepara la misma resolución, pero como IModelBinder en lugar de DefaultModelBinder, por lo que agregar el modelbinder en global.asax es a través de

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

La clase:

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

basado en la publicación @haacked: http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

Korayem
fuente
1
¡+1 para una solución limpia! Puede mejorar aún más la legibilidad de su código cambiando el orden de las returndeclaraciones y negando la condición:if (valueResult == null || string.IsNullOrEmpty(valueResult.AttemptedValue)) return null;
Marius Schulz,
66
Esto no maneja el atributo del controlador [ValidateInput (false)]. Provoca la excepción "Solicitud peligrosa ...".
CodeGrue
2
Para aquellos que reciben la excepción 'Solicitud peligrosa ...', consulte este artículo - blogs.taiga.nl/martijn/2011/09/29/…
GurjeetSinghDB
2
Un compañero de trabajo mío implementó una variación de esto que causó todo tipo de problemas: issues.umbraco.org/issue/U4-6665 Recomendaría devolver nulo y vacío según corresponda en lugar de preferir siempre uno sobre el otro (en su caso, usted siempre devuelve nulo incluso cuando el valor es una cadena vacía).
Nicholas Westby
2
Esto parece romper el [AllowHtml]atributo en las propiedades del modelo (junto con el [ValidateInput(false)]CodeGrue mencionado anteriormente
Mingwei Samuel
43

Una mejora a la respuesta de @takepara.

Algunos estaban en proyecto:

public class NoTrimAttribute : Attribute { }

En el cambio de clase TrimModelBinder

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

a

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

y puede marcar las propiedades que se excluirán del recorte con el atributo [NoTrim].

Anton
fuente
1
¿Cómo podemos implementar algo como este atributo cuando utilizamos el enfoque IModelBinder de @Korayem? En algunas aplicaciones, uso una carpeta de modelo diferente (de terceros) (por ejemplo, S # arp Archeticture's). Me gustaría escribir esto en una DLL privada compartida entre proyectos, por lo que debe ser un enfoque IModelBinder.
Carl Bussema
1
@CarlBussema Aquí hay una pregunta sobre cómo acceder a los atributos desde un IModelBinder. stackoverflow.com/questions/6205176
Mac Attack
44
Creo que es una gran adición, pero la reemplazaría .Cast<object>().Any(a => a.GetType() == typeof(NoTrimAttribute))por .OfType<NoTrimAttribute>().Any(). Solo un poco más limpio.
DBueno
Puse mis atributos en un ensamblaje compartido porque, al igual que con las anotaciones de datos, dichos atributos tienen un alcance de uso más amplio que solo MVC, por ejemplo, nivel de negocios, clientes. Otra observación, el "DisplayFormatAttribute (ConvertEmptyStringToNull)" controla si la cadena recortada se guardará como nula o vacía. El valor predeterminado es verdadero (nulo) que me gusta, pero en caso de que necesite cadenas vacías en su base de datos (con suerte no) puede configurarlo como falso para obtener eso. De todos modos, todo esto es algo bueno, espero que MS extienda sus atributos para incluir recortes y relleno y muchas otras cosas comunes como esa.
Tony Wall
17

Con mejoras en C # 6, ahora puede escribir una carpeta modelo muy compacta que recorta todas las entradas de cadena:

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

Debe incluir esta línea en algún lugar de Application_Start()su Global.asax.csarchivo para usar el modelo de carpeta cuando enlace strings:

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

Creo que es mejor usar un cuaderno de modelo como este, en lugar de anular el cuaderno de modelo predeterminado, porque luego se usará siempre que enlace un string, ya sea directamente como un argumento de método o como una propiedad en una clase de modelo. Sin embargo, si anula la carpeta predeterminada del modelo como sugieren otras respuestas aquí, eso solo funcionará cuando se vinculan propiedades en los modelos, no cuando tiene stringun argumento para un método de acción

Editar: un comentarista preguntó sobre cómo lidiar con la situación cuando un campo no debe ser validado. Mi respuesta original se redujo para tratar solo con la pregunta que el OP había planteado, pero para aquellos que estén interesados, puede lidiar con la validación utilizando el siguiente archivador de modelo extendido:

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();
    }
}
adrian
fuente
Nuevamente, vea los comentarios anteriores. Este ejemplo no maneja el requisito skipValidation de IUnvalidatedValueProvider.
Aaron Hudon el
@adrian, la interfaz IModelBinder solo tiene el método BindModel con el tipo de retorno bool. Entonces, ¿cómo lo usó con el objeto de tipo de retorno aquí?
Magendran V
@MagendranV No estoy seguro de qué interfaz está mirando, pero esta respuesta se basa en IModelBinder en ASP.NET MVC 5, que devuelve un objeto: docs.microsoft.com/en-us/previous-versions/aspnet / ...
adrian
1
@AaronHudon He actualizado mi respuesta para incluir un ejemplo para manejar la validación de omisión
adrian
Si sus campos de contraseña tienen el conjunto de tipos de datos correcto (es decir, [DataType (DataType.Password)]), puede actualizar la última línea de la siguiente manera para que no recorte estos campos: return string.IsNullOrWhiteSpace (intentValue) || bindingContext.ModelMetadata.DataTypeName == "Contraseña"? intentValue: intentValue.Trim ();
trfletch
15

En ASP.Net Core 2 esto funcionó para mí. Estoy usando el [FromBody]atributo en mis controladores y entrada JSON. Para anular el manejo de cadenas en la deserialización JSON, registré mi propio JsonConverter:

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

Y este es el convertidor:

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();
    }
}
Kai G
fuente
¡Tu solución funciona bien! Gracias. Probé las otras soluciones para .Net Core usando IModelBinderProvider, no funcionó.
Cedric Arnould
Excepto en startup.cs, también se puede usar en Model como [JsonConverter (typeof (TrimmingStringConverter))]. Por cierto. ¿hay alguna razón detrás de usar .Insert () en su lugar .Add ()?
eras
@wast Supongo que acabo de hacer .Insert () en lugar de .Add () para asegurar que se ejecute antes que otros Convertidores. No puedo recordar ahora.
Kai G
¿Cuál es la sobrecarga de rendimiento de esto sobre DefaultContractResolver?
Maulik Modi
13

Otra variante de la respuesta de @ takepara pero con un giro diferente:

1) Prefiero el mecanismo de atributo "StringTrim" de aceptación (en lugar del ejemplo de exclusión "NoTrim" de @Anton).

2) Se requiere una llamada adicional a SetModelValue para garantizar que ModelState se complete correctamente y que el patrón de validación / aceptación / rechazo predeterminado se pueda usar de forma normal, es decir, TryUpdateModel (modelo) para aplicar y ModelState.Clear () para aceptar todos los cambios.

Ponga esto en su entidad / biblioteca compartida:

/// <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
{
}

Entonces esto en su aplicación / biblioteca MVC:

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

Si no establece el valor de la propiedad en la carpeta, incluso si no desea cambiar nada, ¡bloqueará esa propiedad de ModelState por completo! Esto se debe a que está registrado como enlace para todos los tipos de cadenas, por lo que parece (en mis pruebas) que el cuaderno predeterminado no lo hará por usted.

Tony Wall
fuente
7

Información adicional para cualquiera que busque cómo hacer esto en ASP.NET Core 1.0. La lógica ha cambiado bastante.

Escribí una publicación de blog sobre cómo hacerlo , explica las cosas en un poco más detallado

Entonces, la solución ASP.NET Core 1.0:

Modelo de carpeta para hacer el recorte real

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

También necesita Model Binder Provider en la última versión, esto indica que si se usa este aglutinante para este modelo

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

Luego tiene que estar registrado en Startup.cs

 services.AddMvc().AddMvcOptions(options => {  
       options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
 });
Tuukka Lindroos
fuente
No funcionó para mí también, todos mis campos son nulos ahora
Cedric Arnould
5

Mientras leía las excelentes respuestas y comentarios anteriores, y cada vez me confundía más, de repente pensé, oye, me pregunto si hay una solución jQuery. Entonces, para otros que, como yo, encuentran ModelBinders un poco desconcertante, les ofrezco el siguiente fragmento de jQuery que recorta los campos de entrada antes de que se envíe el formulario.

    $('form').submit(function () {
        $(this).find('input:text').each(function () {
            $(this).val($.trim($(this).val()));
        })
    });
Eric Nelson
fuente
1
2 cosas: 1: almacenar en caché los objetos del cliente (como $ (this)), 2: nunca puede confiar en las entradas del cliente, pero definitivamente puede confiar en el código del servidor. Entonces su respuesta es una finalización de las respuestas del código del servidor :)
graumanoz
5

En caso de MVC Core

Aglutinante:

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

Proveedor:

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

Función de registro:

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

Registrarse:

service.AddMvc(option => option.AddStringTrimmingProvider())
Vikash Kumar
fuente
+1. Exactamente lo que estaba buscando. ¿Cuál es el propósito del código "binderToFind" en la función Registro?
Brad
Solo estoy tratando de poner un proveedor personalizado con el respaldo de SimpleTypeModelBinderProvidermantener el mismo índice.
Vikash Kumar
Toda la descripción se puede encontrar aquí vikutech.blogspot.in/2018/02/…
Vikash Kumar
3

Tarde a la fiesta, pero el siguiente es un resumen de los ajustes necesarios para MVC 5.2.3 si debe manejar el skipValidationrequisito de los proveedores de valor incorporado.

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());
        ...
    }
Aaron Hudon
fuente
2

No estoy de acuerdo con la solución. Debe anular GetPropertyValue porque ModelState también puede completar los datos de SetProperty. Para capturar los datos sin procesar de los elementos de entrada, escriba esto:

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

}

Filtre por propertyDescriptor PropertyType si realmente solo le interesan los valores de cadena, pero no debería importar porque todo lo que viene es básicamente una cadena.

rudimenter
fuente
2

Para ASP.NET Core , reemplace el ComplexTypeModelBinderProvidercon un proveedor que recorta cadenas.

En su ConfigureServicesmétodo de código de inicio , agregue esto:

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

Definir TrimmingModelBinderProviderasí:

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

La parte fea de esto es copiar y pegar la GetBinderlógica ComplexTypeModelBinderProvider, pero no parece haber ningún enganche que te permita evitar esto.

Edward Brey
fuente
No sé por qué, pero no funciona para ASP.NET Core 1.1.1. Todas las propiedades del objeto modelo que obtengo en la acción del controlador son nulas. El método "SetProperty" se llama nerver.
Waldo
No funcionó para mí, el espacio al comienzo de mi propiedad todavía está allí.
Cedric Arnould
2

Creé proveedores de valores para recortar los valores de los parámetros de la cadena de consulta y los valores del formulario. Esto fue probado con ASP.NET Core 3 y funciona perfectamente.

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

Luego registre las fábricas de proveedores de valor en la ConfigureServices()función en 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();
});
Bassem
fuente
0

Ha habido muchas publicaciones que sugieren un enfoque de atributo. Aquí hay un paquete que ya tiene un atributo trim y muchos otros: Dado.ComponentModel.Mutations o 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();

Después de la llamada a Mutate (), user.UserName será mutado a m@x_speed.01!.

Este ejemplo recortará espacios en blanco y pondrá la cadena en minúsculas. No introduce validación, pero System.ComponentModel.Annotationsse puede usar junto Dado.ComponentModel.Mutations.

Roydukkey
fuente
0

Publiqué esto en otro hilo. En asp.net core 2, fui en una dirección diferente. Usé un filtro de acción en su lugar. En este caso, el desarrollador puede configurarlo globalmente o utilizarlo como un atributo para las acciones que desea aplicar el recorte de cadena. Este código se ejecuta después de que se haya realizado el enlace del modelo y puede actualizar los valores en el objeto del modelo.

Aquí está mi código, primero cree un filtro de acción:

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

Para usarlo, regístrese como filtro global o decore sus acciones con el atributo TrimInputStrings.

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