Najlepszy sposób na przycinanie ciągów po wprowadzeniu danych. Czy powinienem utworzyć segregator na zamówienie?

172

Używam ASP.NET MVC i chciałbym, aby wszystkie pola ciągów wprowadzone przez użytkownika zostały przycięte, zanim zostaną wstawione do bazy danych. A ponieważ mam wiele formularzy wprowadzania danych, szukam eleganckiego sposobu na przycięcie wszystkich ciągów zamiast jawnego przycinania każdej wartości ciągu dostarczonej przez użytkownika. Interesuje mnie, jak i kiedy ludzie przycinają sznurki.

Pomyślałem o stworzeniu spoiwa modelu niestandardowego i przycięciu tam dowolnych wartości ciągów ... w ten sposób cała moja logika przycinania jest zawarta w jednym miejscu. Czy to dobre podejście? Czy są jakieś próbki kodu, które to robią?

Johnny Oshika
źródło

Odpowiedzi:

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

A co z tym kodem?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();

Ustaw zdarzenie global.asax Application_Start.

takepara
źródło
3
po prostu zamieniłbym kod w najbardziej wewnętrznym {} na to dla zwięzłości: string stringValue = (string) value; value = string.IsNullOrEmpty (stringValue)? stringValue: stringValue.Trim ();
Simon_Weaver,
4
To zasługuje na więcej głosów. Właściwie jestem zaskoczony, że zespół MVC nie zdecydował się zaimplementować tego w domyślnym segregatorze modelu ...
Portman
1
@BreckFresen Miałem ten sam problem, będziesz musiał nadpisać metodę BindModel i sprawdzić bindingContext.ModelType dla ciągu, a następnie przyciąć, jeśli tak jest.
Kelly
3
Dla kogokolwiek takiego jak ja, który ma niejednoznaczność w DefaultModelBinder, prawidłowym jest użycie System.Web.Mvc.
GeoffM
3
Jak zmodyfikowałbyś to, aby pozostawić type="password"dane wejściowe nietknięte?
Extragorey
77

To jest ta sama rozdzielczość @takepara, ale jako IModelBinder zamiast DefaultModelBinder, więc dodanie modelbinder w global.asax jest zakończone

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

Klasa:

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

na podstawie posta @haacked: http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

Korayem
źródło
1
+1 za czyste rozwiązanie! Możesz jeszcze bardziej poprawić czytelność swojego kodu, zmieniając kolejność returninstrukcji i negując warunek:if (valueResult == null || string.IsNullOrEmpty(valueResult.AttemptedValue)) return null;
Marius Schulz
6
To nie obsługuje atrybutu kontrolera [ValidateInput (false)]. Powoduje to wyjątek „Niebezpieczne żądanie…”.
CodeGrue,
2
Dla tych, którzy otrzymują wyjątek „Niebezpieczna prośba ...”, zapoznaj się z tym artykułem - blogs.taiga.nl/martijn/2011/09/29/…
GurjeetSinghDB
2
Mój współpracownik zaimplementował odmianę tego, która powodowała różnego rodzaju problemy: Issues.umbraco.org/issue/U4-6665 Zalecałbym zwrócenie pustego i pustego w stosownym przypadku, zamiast zawsze preferować jeden nad drugim (w twoim przypadku zawsze zwracają wartość null, nawet jeśli wartość jest pustym ciągiem).
Nicholas Westby
2
Wydaje się, że powoduje to uszkodzenie [AllowHtml]atrybutu we właściwościach modelu (wraz z [ValidateInput(false)]wyżej wymienionym kodem CodeGrue
Mingwei Samuel
43

Jedno ulepszenie odpowiedzi @takepara.

Gdzieś w projekcie:

public class NoTrimAttribute : Attribute { }

W TrimModelBinder zmiana klasy

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

do

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

i możesz oznaczyć właściwości, które mają być wykluczone z przycinania za pomocą atrybutu [NoTrim].

Anton
źródło
1
W jaki sposób możemy zaimplementować coś takiego jak ten atrybut, używając podejścia IModelBinder autorstwa @Korayem? W niektórych aplikacjach używam innego segregatora (innej firmy) modelu (np. S # arp Archeticture). Chciałbym napisać to w prywatnej bibliotece DLL współdzielonej między projektami, więc musi to być podejście IModelBinder.
Carl Bussema
1
@CarlBussema Oto pytanie dotyczące dostępu do atrybutów z poziomu IModelBinder. stackoverflow.com/questions/6205176
Mac Attack
4
Myślę, że to świetny dodatek, ale chciałbym wymienić .Cast<object>().Any(a => a.GetType() == typeof(NoTrimAttribute))ze .OfType<NoTrimAttribute>().Any(). Tylko trochę czystszy.
DBueno,
Umieszczam swoje atrybuty w zestawie współdzielonym, ponieważ podobnie jak w przypadku adnotacji danych, takie atrybuty mają szerszy zakres użycia niż tylko MVC, np. Warstwa biznesowa, klienci. Jeszcze jedna obserwacja, „DisplayFormatAttribute (ConvertEmptyStringToNull)” kontroluje, czy przycięty ciąg zostanie zapisany jako pusty, czy jako pusty. Wartość domyślna to true (null), co mi się podoba, ale w przypadku, gdy potrzebujesz pustych ciągów w swojej bazie danych (miejmy nadzieję, że nie), możesz ustawić wartość false, aby to uzyskać. W każdym razie to wszystko jest dobre, mam nadzieję, że MS rozszerzy swoje atrybuty o przycinanie i wyściółkę i wiele innych podobnych rzeczy.
Tony Wall
17

Dzięki ulepszeniom w C # 6 możesz teraz napisać bardzo kompaktowy spinacz modelu, który będzie przycinał wszystkie dane wejściowe ciągu:

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

Trzeba to gdzieś w tej linii Application_Start()w Global.asax.cspliku, aby użyć spinacza modelu podczas wiązania strings:

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

Uważam, że lepiej jest użyć spinacza modelu, takiego jak ten, zamiast nadpisywać domyślny spinacz modelu, ponieważ będzie on używany zawsze, gdy będziesz wiązać a string, niezależnie od tego, czy jest to bezpośrednio jako argument metody, czy jako właściwość w klasie modelu. Jeśli jednak zastąpisz domyślny spinacz modelu, jak sugerują inne odpowiedzi tutaj, będzie to działać tylko w przypadku wiązania właściwości w modelach, a nie wtedy, gdy masz stringargument do metody akcji

Edycja: komentator zapytany o rozwiązanie sytuacji, w której pole nie powinno być walidowane. Moja pierwotna odpowiedź została ograniczona tylko do pytania zadanego przez OP, ale dla zainteresowanych możesz poradzić sobie z walidacją, używając następującego rozszerzonego segregatora:

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
źródło
Zobacz komentarze powyżej. Ten przykład nie obsługuje wymagania skipValidation IUnvalidatedValueProvider.
Aaron Hudon
@adrian, Interfejs IModelBinder ma tylko metodę BindModel z zwracanym typem bool. Jak więc użyłeś tutaj obiektu typu zwracanego?
Magendran, V
@MagendranV Nie jestem pewien, na który interfejs patrzysz, ale ta odpowiedź jest oparta na IModelBinder w ASP.NET MVC 5, który zwraca obiekt: docs.microsoft.com/en-us/previous-versions/aspnet /…
adrian
1
@AaronHudon Zaktualizowałem moją odpowiedź, aby zawierała przykład obsługi pomijania walidacji
adrian
Jeśli pola hasła mają ustawiony prawidłowy typ danych (np. [DataType (DataType.Password)]), możesz zaktualizować ostatnią linię w następujący sposób, aby nie przycinała tych pól: return string.IsNullOrWhiteSpace (próbowana wartość) || bindingContext.ModelMetadata.DataTypeName == "Hasło"? probedValue: probedValue.Trim ();
trfletch
15

W ASP.Net Core 2 to zadziałało. Używam [FromBody]atrybutu w moich kontrolerach i danych wejściowych JSON. Aby przesłonić obsługę ciągów w deserializacji JSON, zarejestrowałem własny JsonConverter:

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

A to jest konwerter:

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.
źródło
Twoje rozwiązanie działa dobrze! Dzięki. Wypróbowałem inne rozwiązania dla .Net Core przy użyciu IModelBinderProvider, nie działało.
Cedric Arnould
Z wyjątkiem startup.cs, może być również używany w modelu jako [JsonConverter (typeof (TrimmingStringConverter))]. Przy okazji. czy jest jakiś powód za używaniem .Insert () zamiast .Add ()?
zmarło
@wast Chyba właśnie zrobiłem .Insert () zamiast .Add (), aby upewnić się, że zostanie uruchomiony przed innymi konwerterami. Nie pamiętam teraz.
Kai G
Jaki jest narzut wydajności tego w stosunku do DefaultContractResolver?
Maulik Modi
13

Kolejny wariant odpowiedzi @ takepara, ale z innym akcentem:

1) Wolę mechanizm atrybutu opt-in „StringTrim” (zamiast przykładowego atrybutu @Anton „NoTrim” rezygnacji).

2) Wymagane jest dodatkowe wywołanie SetModelValue, aby upewnić się, że ModelState jest poprawnie wypełniony, a domyślny wzorzec walidacji / akceptacji / odrzucania może być używany normalnie, tj. TryUpdateModel (model) do zastosowania i ModelState.Clear () do akceptowania wszystkich zmian.

Umieść to w swojej encji / udostępnianej bibliotece:

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

Następnie w Twojej aplikacji / bibliotece 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;
    }
}

Jeśli nie ustawisz wartości właściwości w segregatorze, nawet jeśli nie chcesz niczego zmieniać, całkowicie zablokujesz tę właściwość przed ModelState! Dzieje się tak, ponieważ jesteś zarejestrowany jako wiążący wszystkie typy ciągów, więc wydaje się (w moich testach), że domyślny spinacz nie zrobi tego za Ciebie.

Tony Wall
źródło
7

Dodatkowe informacje dla każdego, kto szuka, jak to zrobić w ASP.NET Core 1.0. Logika bardzo się zmieniła.

Napisałem post na blogu o tym, jak to zrobić , wyjaśnia to nieco bardziej szczegółowo

Więc rozwiązanie ASP.NET Core 1.0:

Modelowy segregator do faktycznego przycinania

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

Potrzebujesz również Model Binder Provider w najnowszej wersji, to mówi, że ten segregator powinien być używany dla tego modelu

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

Następnie należy go zarejestrować w Startup.cs

 services.AddMvc().AddMvcOptions(options => {  
       options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
 });
Tuukka Lindroos
źródło
Dla mnie też to nie zadziałało, wszystkie moje pola są teraz puste
Cedric Arnould
5

Czytając doskonałe odpowiedzi i komentarze powyżej i coraz bardziej zdezorientowany, nagle pomyślałem: hej, zastanawiam się, czy istnieje rozwiązanie jQuery. Dlatego dla innych, którzy, tak jak ja, uważają ModelBinders za nieco oszałamiające, oferuję następujący fragment kodu jQuery, który przycina pola wejściowe przed przesłaniem formularza.

    $('form').submit(function () {
        $(this).find('input:text').each(function () {
            $(this).val($.trim($(this).val()));
        })
    });
Eric Nelson
źródło
1
Dwie rzeczy: 1 - Buforuj obiekty klienta (takie jak $ (this)), 2 - Nigdy nie możesz polegać na danych wejściowych klienta, ale zdecydowanie możesz polegać na kodzie serwera. Twoja odpowiedź jest więc uzupełnieniem odpowiedzi na kod serwera :)
graumanoz
5

W przypadku MVC Core

Spoiwo:

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

Dostawca:

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

Funkcja rejestracji:

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

Zarejestrować:

service.AddMvc(option => option.AddStringTrimmingProvider())
Vikash Kumar
źródło
+1. Dokładnie to, czego szukałem. Jaki jest cel kodu „binderToFind” w funkcji Rejestracja?
Brad
Po prostu próbuję umieścić niestandardowego dostawcę z rezerwą SimpleTypeModelBinderProviderprzez utrzymanie tego samego indeksu.
Vikash Kumar
Cały opis można znaleźć tutaj vikutech.blogspot.in/2018/02/…
Vikash Kumar
3

Spóźniłem się na imprezę, ale poniżej znajduje się podsumowanie korekt wymaganych dla MVC 5.2.3, jeśli masz sprostać skipValidationwymaganiom wbudowanych dostawców wartości.

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
źródło
2

Nie zgadzam się z rozwiązaniem. Należy zastąpić GetPropertyValue, ponieważ dane dla SetProperty mogą być również wypełnione przez ModelState. Aby złapać surowe dane z elementów wejściowych, napisz to:

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

}

Filtruj według propertyDescriptor PropertyType, jeśli naprawdę interesują Cię tylko wartości ciągów, ale nie powinno to mieć znaczenia, ponieważ wszystko, co pojawia się, jest w zasadzie ciągiem.

rudimenter
źródło
2

W przypadku ASP.NET Core Zastąp ComplexTypeModelBinderProviderdostawcę, który przycina ciągi.

W ConfigureServicesmetodzie kodu startowego dodaj to:

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

Zdefiniuj w TrimmingModelBinderProviderten sposób:

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

Brzydką częścią tego jest kopiowanie i wklejanie GetBinderlogiki z ComplexTypeModelBinderProvider, ale wydaje się, że nie ma żadnego haka, który pozwoliłby ci tego uniknąć.

Edward Brey
źródło
Nie wiem dlaczego, ale to nie działa w przypadku ASP.NET Core 1.1.1. Wszystkie właściwości obiektu modelu, które otrzymuję w akcji kontrolera, są zerowe. Metoda „SetProperty” jest wywoływana przez nerver.
Waldo
Dla mnie nie działało, przestrzeń na początku mojej posiadłości wciąż tam jest.
Cedric Arnould
2

Utworzyłem dostawców wartości, aby przyciąć wartości parametrów ciągu zapytania i wartości formularza. Zostało to przetestowane z ASP.NET Core 3 i działa doskonale.

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

Następnie zarejestruj fabryki dostawców wartości w ConfigureServices()funkcji w 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
źródło
0

Pojawiło się wiele postów sugerujących podejście do atrybutów. Oto pakiet, który ma już atrybut trim i wiele innych: Dado.ComponentModel.Mutations lub 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();

Po wywołaniu Mutate () użytkownik.UserName zostanie zmieniony na m@x_speed.01!.

Ten przykład spowoduje obcięcie białych znaków i małych liter w ciągu. Nie wprowadza walidacji, ale System.ComponentModel.Annotationsmoże być używany razem z Dado.ComponentModel.Mutations.

roydukkey
źródło
0

Opublikowałem to w innym wątku. W asp.net core 2 poszedłem w innym kierunku. Zamiast tego użyłem filtra akcji. W takim przypadku programista może ustawić go globalnie lub użyć jako atrybutu dla działań, które chce przyciąć ciąg. Ten kod jest uruchamiany po utworzeniu powiązania modelu i może aktualizować wartości w obiekcie modelu.

Oto mój kod, najpierw utwórz filtr akcji:

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

Aby go użyć, zarejestruj się jako filtr globalny lub udekoruj swoje akcje atrybutem TrimInputStrings.

[TrimInputStrings]
public IActionResult Register(RegisterViewModel registerModel)
{
    // Some business logic...
    return Ok();
}
Marcos de Aguiar
źródło