数据输入后修剪字符串的最佳方法。我应该创建一个自定义模型绑定器吗?

我正在使用 ASP.NET MVC,我希望所有用户输入的字符串字段在插入数据库之前进行修剪。由于我有许多数据输入表单,我正在寻找一种优雅的方法来修剪所有字符串,而不是显式地修剪每个用户提供的字符串值。我很想知道人们是如何以及什么时候修改关系的。

我想过也许创建一个自定义模型绑定器,并在那里修剪任何字符串值... ... 这样,我所有的修剪逻辑都包含在一个地方。这是个好办法吗?有什么代码样本可以做到这一点吗?

59186 次浏览
  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);
}
}

这个密码怎么样?

ModelBinders.Binders.DefaultBinder = new TrimModelBinder();

设置 global.asax Application _ Start 事件。

这是@takpara 相同的分辨率,但是作为一个 IModelBinder 而不是 DefaultModelBinder,因此在 global.asax 中添加 Modelbinder 是通过的

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

课程:

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

基于@haked post: Http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx

对@takpara 的回答有一个改进。

有些人在计划中:

public class NoTrimAttribute : Attribute { }

在 TrimModelBinder 类更改中

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

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

并且可以使用[ NoTrim ]属性标记要排除的属性。

我不同意这个解决方案。 应重写 GetPropertyValue,因为 SetProperty 的数据也可以由 ModelState 填充。 要捕获来自输入元素的原始数据,请编写如下代码:

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


}

如果您真的只对字符串值感兴趣,那么使用 PropertyDescriptorPropertyType 进行筛选,但这并不重要,因为输入的所有内容基本上都是一个字符串。

@ takpara 的回答的另一种变体,但有一个不同的转折:

1)我更喜欢 opt-in“ StringTrim”属性机制(而不是@Anton 的 opt-out“ NoTrim”示例)。

2)需要额外调用 SetModelValue 来确保 ModelState 被正确填充,并且默认的验证/接受/拒绝模式可以正常使用,即应用 TryUpdateModel (model)和 ModelState。Clear ()接受所有更改。

将其放入实体/共享库中:

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

在你的 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;
}
}

如果您不在活页夹中设置属性值,即使您不想更改任何内容,您也会完全阻止 ModelState 的属性!这是因为您被注册为绑定所有字符串类型,所以(在我的测试中)似乎默认绑定器不会为您这样做。

在阅读上面的精彩答案和评论时,我越来越感到困惑,我突然想到,嘿,我想知道是否有一个 jQuery 解决方案。因此,对于像我这样觉得 ModelBinders 有点令人困惑的人,我提供了以下 jQuery 片段,它在表单提交之前对输入字段进行了整理。

    $('form').submit(function () {
$(this).find('input:text').each(function () {
$(this).val($.trim($(this).val()));
})
});

对于任何在 ASP.NET Core 1.0中搜索如何做到这一点的人来说,额外的信息。逻辑已经发生了很大的变化。

我写了一篇关于如何做的博客文章,它解释了一些更详细的东西

因此 ASP.NET Core 1.0解决方案:

模型粘合剂做实际的修剪

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

您还需要在最新版本中使用 ModelBinderProvider,这表明应该将此绑定器用于此模型

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

那它必须在 Startup.cs 注册

 services.AddMvc().AddMvcOptions(options => {
options.ModelBinderProviders.Insert(0, new TrimmingModelBinderProvider());
});

随着 C # 6的改进,您现在可以编写一个非常紧凑的模型绑定器,它将修剪所有字符串输入:

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

在绑定 string时,需要在 Global.asax.cs文件的 Application_Start()中包含这一行,以使用模型绑定器:

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

我发现最好像这样使用模型绑定器,而不是重写默认的模型绑定器,因为这样无论何时绑定 string,都会使用它,无论它是直接作为方法参数还是作为模型类的属性。但是,如果您覆盖默认的模型绑定器,因为其他答案在这里建议,这将 只有工作时绑定属性的模型,没有时,您有一个 string作为参数的动作方法

编辑: 一个评论者询问如何处理一个字段不应该被验证的情况。我最初的回答被简化为只处理 OP 提出的问题,但是对于那些感兴趣的人,你可以通过使用以下扩展的模型绑定器来处理验证:

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

更新: 这个答案对于最新版本的 ASP.NET Core 来说已经过时了。改用 巴森的回答


对于 ASP.NET 核心,用一个修剪字符串的提供程序替换 ComplexTypeModelBinderProvider

在启动代码 ConfigureServices方法中,添加以下内容:

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

像这样定义 TrimmingModelBinderProvider:

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

丑陋的部分是从 ComplexTypeModelBinderProvider复制和粘贴 GetBinder逻辑,但似乎没有任何钩子让您避免这一点。

虽然晚了一些,但是如果你想要处理内建值提供者的 skipValidation要求,以下是 MVC 5.2.3所需要的调整的总结。

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

在 MVC 核心的情况下

活页夹:

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

提供者:

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

登记功能:

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

登记册:

service.AddMvc(option => option.AddStringTrimmingProvider())

有很多帖子建议使用属性方法。这里是一个包,已经有一个修剪属性和许多其他: 达多,组件模型,变异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();

在调用 Mutate ()之后,user.UserName 将被突变为 m@x_speed.01!

此示例将修剪空格并将字符串大小写为小写。它不引入验证,但是 System.ComponentModel.Annotations可以与 Dado.ComponentModel.Mutations一起使用。

ASP.Net Core 2中,这对我很有用。我在控制器和 JSON 输入中使用 [FromBody]属性。为了覆盖 JSON 反序列化中的字符串处理,我注册了自己的 JsonConverter:

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

这是转换器:

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

我在另一个帖子里发了这个。在 asp.net 核心2中,我选择了一个不同的方向。我改用了动作过滤器。在这种情况下,开发人员可以将其全局设置,或者将其用作他/她希望应用字符串修剪的操作的属性。此代码在模型绑定完成后运行,并且可以更新模型对象中的值。

下面是我的代码,首先创建一个操作过滤器:

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

要使用它,可以注册为全局筛选器,也可以使用 TrimInputStrings 属性装饰您的操作。

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

我创建了值提供程序来修剪查询字符串参数值和表单值。这是测试与 ASP.NET 核心3和工程完美。

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

然后在 Startup.cs 中的 ConfigureServices()函数中注册值提供者工厂

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

好吧,我有这个东西,它有点工作:

class TrimmingModelBinder : IModelBinder
{
public Task BindModelAsync (ModelBindingContext ctx)
{
if
(
ctx .ModelName is string name
&& ctx .ValueProvider .GetValue (name) .FirstValue is string v)
ctx .ModelState .SetModelValue
(
name,
new ValueProviderResult
((ctx .Result = ModelBindingResult .Success (v .Trim ())) .Model as string));
return Task .CompletedTask; }}


class AutoTrimAttribute : ModelBinderAttribute
{
public AutoTrimAttribute ()
{ this .BinderType = typeof (TrimmingModelBinder); }}

不过,遗憾的是没有这方面的标准特性。

我把@Kai G 的答案改编成了 System.Text.Json:

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


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


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


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