Meilleur moyen de couper les chaînes après la saisie des données. Dois-je créer un classeur de modèles personnalisé?

172

J'utilise ASP.NET MVC et j'aimerais que tous les champs de chaîne saisis par l'utilisateur soient coupés avant d'être insérés dans la base de données. Et comme j'ai de nombreux formulaires de saisie de données, je recherche un moyen élégant de couper toutes les chaînes au lieu de couper explicitement chaque valeur de chaîne fournie par l'utilisateur. Je suis intéressé de savoir comment et quand les gens coupent les chaînes.

J'ai pensé à peut-être créer un classeur de modèle personnalisé et couper les valeurs de chaîne là-bas ... de cette façon, toute ma logique de coupe est contenue au même endroit. Est-ce une bonne approche? Existe-t-il des exemples de code qui font cela?

Johnny Oshika
la source

Réponses:

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

Et ce code?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();

Définissez l'événement global.asax Application_Start.

Takepara
la source
3
je remplacerais simplement le code dans le plus intérieur {} par ceci pour la brièveté: string stringValue = (string) value; value = string.IsNullOrEmpty (stringValue)? stringValue: stringValue.Trim ();
Simon_Weaver
4
Cela mérite plus de votes positifs. Je suis en fait surpris que l'équipe MVC n'ait pas choisi de mettre en œuvre cela dans le classeur de modèles par défaut ...
Portman
1
@BreckFresen J'ai eu le même problème, vous devrez remplacer la méthode BindModel et vérifier le bindingContext.ModelType pour une chaîne puis couper si c'est le cas.
Kelly
3
Pour quiconque comme moi obtient une ambiguïté sur DefaultModelBinder, le bon utilise System.Web.Mvc.
GeoffM
3
Comment modifieriez-vous cela pour laisser les type="password"entrées intactes?
Extragorey
77

C'est @takepara la même résolution mais comme IModelBinder au lieu de DefaultModelBinder de sorte que l'ajout du modelbinder dans global.asax se fasse

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

La classe:

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

basé sur @haacked post: http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

Korayem
la source
1
+1 pour une solution propre! Vous pouvez améliorer encore plus la lisibilité de votre code en modifiant l'ordre des returninstructions et en annulant la condition:if (valueResult == null || string.IsNullOrEmpty(valueResult.AttemptedValue)) return null;
Marius Schulz
6
Cela ne gère pas l'attribut du contrôleur [ValidateInput (false)]. Cela provoque l'exception "Requête dangereuse ...".
CodeGrue
2
Pour ceux qui reçoivent une exception «Requête dangereuse ...», reportez-vous à cet article - blogs.taiga.nl/martijn/2011/09/29/…
GurjeetSinghDB
2
Un de mes collègues a mis en œuvre une variante de celui-ci qui a causé toutes sortes de problèmes: issues.umbraco.org/issue/U4-6665 Je recommanderais de renvoyer null et vide le cas échéant plutôt que de toujours préférer l'un à l'autre (dans votre cas, vous renvoie toujours null même lorsque la valeur est une chaîne vide).
Nicholas West du
2
Cela semble casser l' [AllowHtml]attribut sur les propriétés du modèle (avec le [ValidateInput(false)]codeGrue mentionné ci-dessus
Mingwei Samuel
43

Une amélioration de la réponse @takepara.

Certains étaient en projet:

public class NoTrimAttribute : Attribute { }

Dans le changement de classe TrimModelBinder

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

à

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

et vous pouvez marquer les propriétés à exclure du découpage avec l'attribut [NoTrim].

Anton
la source
1
Comment pouvons-nous implémenter quelque chose comme cet attribut lors de l'utilisation de l'approche IModelBinder par @Korayem? Dans certaines applications, j'utilise un classeur de modèle différent (tiers) (par exemple, S # arp Archeticture). Je voudrais écrire cela dans une DLL privée partagée entre les projets, il doit donc s'agir d'une approche IModelBinder.
Carl Bussema
1
@CarlBussema Voici une question sur l'accès aux attributs depuis un IModelBinder. stackoverflow.com/questions/6205176
Attaque Mac
4
Je pense que c'est un excellent ajout, mais je remplacerais le .Cast<object>().Any(a => a.GetType() == typeof(NoTrimAttribute))par .OfType<NoTrimAttribute>().Any(). Juste un peu plus propre.
DBueno
Je place mes attributs dans un assembly partagé car, comme pour les annotations de données, ces attributs ont une portée d'utilisation plus large que le MVC, par exemple le niveau métier, les clients. Une autre observation, le «DisplayFormatAttribute (ConvertEmptyStringToNull)» contrôle si la chaîne découpée sera enregistrée comme null ou une chaîne vide. La valeur par défaut est true (null), ce que j'aime, mais au cas où vous auriez besoin de chaînes vides dans votre base de données (espérons que non), vous pouvez la définir sur false pour l'obtenir. Quoi qu'il en soit, tout cela est bon, j'espère que MS étendra ses attributs pour inclure le rognage et le rembourrage et beaucoup d'autres choses courantes comme ça.
Tony Wall
17

Avec les améliorations apportées à C # 6, vous pouvez désormais écrire un classeur de modèles très compact qui réduira toutes les entrées de chaîne:

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

Vous devez inclure cette ligne quelque part dans Application_Start()votre Global.asax.csfichier pour utiliser le classeur de modèle lors de la liaison de strings:

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

Je trouve qu'il est préférable d'utiliser un classeur de modèle comme celui-ci, plutôt que de remplacer le classeur de modèle par défaut, car il sera alors utilisé chaque fois que vous liez a string, que ce soit directement comme argument de méthode ou comme propriété sur une classe de modèle. Cependant, si vous remplacez le classeur de modèle par défaut comme le suggèrent d'autres réponses ici, cela ne fonctionnera que lors de la liaison de propriétés sur des modèles, pas lorsque vous avez un stringcomme argument à une méthode d'action

Edit: un commentateur a demandé comment gérer la situation où un champ ne devrait pas être validé. Ma réponse initiale a été réduite pour ne traiter que de la question posée par l'OP, mais pour ceux qui sont intéressés, vous pouvez gérer la validation en utilisant le classeur de modèles étendu suivant:

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
la source
Encore une fois, voir les commentaires ci-dessus. Cet exemple ne gère pas l'exigence skipValidation de IUnvalidatedValueProvider.
Aaron Hudon
@adrian, L'interface IModelBinder n'a que la méthode BindModel avec le type de retour bool. Alors, comment avez-vous utilisé avec un objet de type de retour ici?
Magendran V
@MagendranV Je ne sais pas quelle interface vous regardez, mais cette réponse est basée sur IModelBinder dans ASP.NET MVC 5, qui renvoie un objet: docs.microsoft.com/en-us/previous-versions/aspnet /…
adrian
1
@AaronHudon J'ai mis à jour ma réponse pour inclure un exemple pour gérer le saut de validation
adrian
Si vos champs de mot de passe ont le bon jeu de types de données (c'est-à-dire [DataType (DataType.Password)]), vous pouvez mettre à jour la dernière ligne comme suit afin qu'elle ne rogne pas ces champs: return string.IsNullOrWhiteSpace (TryValue) || bindingContext.ModelMetadata.DataTypeName == "Mot de passe"? TryValue: TryValue.Trim ();
trfletch
15

Dans ASP.Net Core 2, cela a fonctionné pour moi. J'utilise l' [FromBody]attribut dans mes contrôleurs et l'entrée JSON. Pour remplacer la gestion des chaînes dans la désérialisation JSON, j'ai enregistré mon propre JsonConverter:

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

Et voici le convertisseur:

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
la source
Votre solution fonctionne bien! Merci. J'ai essayé les autres solutions pour .Net Core en utilisant IModelBinderProvider, cela n'a pas fonctionné.
Cedric Arnould
Sauf dans startup.cs, il peut également être utilisé dans Model comme [JsonConverter (typeof (TrimmingStringConverter))]. Btw. y a-t-il une raison d'utiliser .Insert () à la place .Add ()?
wast le
@wast Je suppose que je viens de faire .Insert () au lieu de .Add () pour m'assurer qu'il est exécuté avant les autres convertisseurs. Je ne me souviens plus maintenant.
Kai G
Quelle est la surcharge de performance de ce sur DefaultContractResolver?
Maulik Modi
13

Une autre variante de la réponse de @ takepara mais avec une tournure différente:

1) Je préfère le mécanisme d'attribut opt-in "StringTrim" (plutôt que l'exemple opt-out "NoTrim" de @Anton).

2) Un appel supplémentaire à SetModelValue est nécessaire pour s'assurer que ModelState est correctement rempli et que le modèle de validation / acceptation / rejet par défaut peut être utilisé normalement, c'est-à-dire TryUpdateModel (modèle) pour appliquer et ModelState.Clear () pour accepter toutes les modifications.

Mettez ceci dans votre entité / bibliothèque partagée:

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

Puis ceci dans votre application / bibliothèque 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 vous ne définissez pas la valeur de la propriété dans le classeur, même si vous ne voulez rien changer, vous bloquerez complètement cette propriété de ModelState! C'est parce que vous êtes enregistré comme liant tous les types de chaînes, il semble donc (dans mes tests) que le classeur par défaut ne le fera pas pour vous alors.

Tony Wall
la source
7

Informations supplémentaires pour toute personne cherchant comment faire cela dans ASP.NET Core 1.0. La logique a beaucoup changé.

J'ai écrit un article de blog sur la façon de le faire , il explique les choses un peu plus en détail

Donc, solution ASP.NET Core 1.0:

Reliure modèle pour effectuer la coupe proprement dite

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

Vous avez également besoin de Model Binder Provider dans la dernière version, cela indique que si ce classeur doit être utilisé pour ce modèle

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

Ensuite, il doit être enregistré dans Startup.cs

 services.AddMvc().AddMvcOptions(options => {  
       options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
 });
Tuukka Lindroos
la source
Cela n'a pas fonctionné pour moi aussi, tous mes champs sont nuls maintenant
Cedric Arnould
5

En lisant les excellentes réponses et commentaires ci-dessus, et devenant de plus en plus confus, j'ai soudainement pensé, hé, je me demande s'il existe une solution jQuery. Donc, pour ceux qui, comme moi, trouvent ModelBinders un peu déroutant, je propose l'extrait de code jQuery suivant qui rogne les champs de saisie avant que le formulaire ne soit soumis.

    $('form').submit(function () {
        $(this).find('input:text').each(function () {
            $(this).val($.trim($(this).val()));
        })
    });
Eric Nelson
la source
1
2 choses: 1 - Mettez en cache vos objets client (comme $ (this)), 2 - Vous ne pouvez jamais compter sur les entrées client, mais vous pouvez certainement vous fier au code serveur. Donc, votre réponse est un complément aux réponses du code du serveur :)
graumanoz
5

Dans le cas de MVC Core

Classeur:

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

Fournisseur:

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

Fonction d'enregistrement:

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

S'inscrire:

service.AddMvc(option => option.AddStringTrimmingProvider())
Vikash Kumar
la source
+1. Exactement ce que je cherchais. À quoi sert le code "binderToFind" dans la fonction d'enregistrement?
Brad
J'essaie juste de mettre un fournisseur personnalisé avec la solution de secours SimpleTypeModelBinderProvideren conservant le même index.
Vikash Kumar
La description complète peut être trouvée ici vikutech.blogspot.in/2018/02/…
Vikash Kumar
3

À la fin de la partie, mais ce qui suit est un résumé des ajustements requis pour MVC 5.2.3 si vous devez gérer l' skipValidationexigence de l'accumulation dans les fournisseurs de valeur.

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
la source
2

Je ne suis pas d'accord avec la solution. Vous devez remplacer GetPropertyValue car les données de SetProperty peuvent également être remplies par ModelState. Pour capturer les données brutes des éléments d'entrée, écrivez ceci:

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

}

Filtrez par propertyDescriptor PropertyType si vous n'êtes vraiment intéressé que par les valeurs de chaîne, mais cela ne devrait pas avoir d'importance car tout ce qui entre est essentiellement une chaîne.

rudimenter
la source
2

Pour ASP.NET Core , remplacez le ComplexTypeModelBinderProviderpar un fournisseur qui coupe les chaînes.

Dans votre ConfigureServicesméthode de code de démarrage , ajoutez ceci:

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

Définissez TrimmingModelBinderProvidercomme ceci:

/// <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 partie laide de ceci est le copier-coller de la GetBinderlogique de ComplexTypeModelBinderProvider, mais il ne semble pas y avoir de crochet pour vous permettre d'éviter cela.

Edward Brey
la source
Je ne sais pas pourquoi, mais cela ne fonctionne pas pour ASP.NET Core 1.1.1. Toutes les propriétés de l'objet modèle que j'obtiens dans l'action du contrôleur sont nulles. La méthode "SetProperty" est appelée nerver.
Waldo
Cela n'a pas fonctionné pour moi, l'espace au début de ma propriété est toujours là.
Cedric Arnould
2

J'ai créé des fournisseurs de valeur pour réduire les valeurs des paramètres de la chaîne de requête et les valeurs du formulaire. Cela a été testé avec ASP.NET Core 3 et fonctionne parfaitement.

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

Enregistrez ensuite les fabriques de fournisseurs de valeur dans la ConfigureServices()fonction dans 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
la source
0

Il y a eu de nombreux articles suggérant une approche attributaire. Voici un package qui a déjà un attribut trim et bien d'autres: Dado.ComponentModel.Mutations ou 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();

Après l'appel à Mutate (), user.UserName sera muté en m@x_speed.01!.

Cet exemple réduira les espaces et mettra la chaîne en minuscules. Il n'introduit pas de validation, mais System.ComponentModel.Annotationspeut être utilisé à côté Dado.ComponentModel.Mutations.

roydukkey
la source
0

J'ai posté ceci dans un autre fil. Dans asp.net core 2, je suis allé dans une direction différente. J'ai utilisé un filtre d'action à la place. Dans ce cas, le développeur peut soit le définir globalement, soit l'utiliser comme attribut pour les actions qu'il / elle souhaite appliquer le découpage de chaîne. Ce code s'exécute après la liaison de modèle et peut mettre à jour les valeurs dans l'objet de modèle.

Voici mon code, créez d'abord un filtre d'action:

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

Pour l'utiliser, enregistrez-vous en tant que filtre global ou décorez vos actions avec l'attribut TrimInputStrings.

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