我正在使用ASP.NET MVC,我希望在将所有用户输入的字符串字段插入数据库之前先对其进行修剪。由于我有许多数据输入表单,因此我正在寻找一种修整所有字符串的方法,而不是显式修整每个用户提供的字符串值。我很想知道人们何时以及如何修剪琴弦。
我考虑过可能要创建一个自定义模型联编程序并在那里修剪任何字符串值...那样,我所有的修剪逻辑都包含在一个地方。这是一个好方法吗?有没有执行此操作的代码示例?
我正在使用ASP.NET MVC,我希望在将所有用户输入的字符串字段插入数据库之前先对其进行修剪。由于我有许多数据输入表单,因此我正在寻找一种修整所有字符串的方法,而不是显式修整每个用户提供的字符串值。我很想知道人们何时以及如何修剪琴弦。
我考虑过可能要创建一个自定义模型联编程序并在那里修剪任何字符串值...那样,我所有的修剪逻辑都包含在一个地方。这是一个好方法吗?有没有执行此操作的代码示例?
Answers:
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事件。
type="password"
保持输入不变?
这是@takepara相同的分辨率,但与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();
}
}
基于@haacked帖子:http ://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx
return
语句的顺序并取消条件来进一步提高代码的可读性:if (valueResult == null || string.IsNullOrEmpty(valueResult.AttemptedValue)) return null;
[AllowHtml]
模型属性的属性(以及[ValidateInput(false)]
上面提到的CodeGrue
@takepara答案的一项改进。
在项目中:
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]属性标记要修剪的属性。
.Cast<object>().Any(a => a.GetType() == typeof(NoTrimAttribute))
用.OfType<NoTrimAttribute>().Any()
。只是一点点清洁。
通过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();
}
}
您需要Application_Start()
在Global.asax.cs
文件中的某处包含此行,以便在绑定string
s 时使用模型绑定器:
ModelBinders.Binders.Add(typeof(string), new TrimStringModelBinder());
我发现最好使用这样的模型联编程序,而不是覆盖默认的模型联编程序,因为这样,无论您将a string
直接绑定为方法参数还是模型类的属性,都将使用它。但是,如果您按照此处建议的其他答案覆盖默认的模型绑定器,则仅当绑定模型上的属性时才有效,而当您string
将action方法的参数作为参数时才起作用
编辑:评论者问有关不应该验证字段时的处理情况。我最初的回答被简化为仅处理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 2中,这对我有用。我[FromBody]
在控制器和JSON输入中使用属性。为了覆盖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();
}
}
@takepara答案的另一个变体,但又有不同的变化:
1)我更喜欢选择加入“ StringTrim”属性机制(而不是@Anton的选择退出“ 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!这是因为您已注册为绑定所有字符串类型,因此(在我的测试中)默认绑定器似乎不会为您执行此操作。
任何在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);
}
}
此外,您还需要最新版本的Model Binder Provider,这表明该模型应使用此Binder
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());
});
在阅读了上面的出色答案和评论并变得越来越困惑的同时,我突然想到,嘿,我想知道是否有jQuery解决方案。因此,对于像我一样感到有些困惑的其他人,我提供了以下jQuery代码段,这些代码段在提交表单之前修剪了输入字段。
$('form').submit(function () {
$(this).find('input:text').each(function () {
$(this).val($.trim($(this).val()));
})
});
如果是MVC Core
活页夹:
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())
SimpleTypeModelBinderProvider
通过保留相同的索引来使自定义提供程序具有后备功能。
晚了,但是如果要处理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());
...
}
我不同意该解决方案。您应该重写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();
}
}
如果您真的只对字符串值感兴趣,请按propertyDescriptor PropertyType进行过滤,但不要紧,因为其中的所有内容基本上都是字符串。
对于ASP.NET Core,将其替换为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;
}
}
丑陋的部分是GetBinder
逻辑的复制和粘贴ComplexTypeModelBinderProvider
,但是似乎没有任何挂钩可以避免这种情况。
我创建了值提供程序来修剪查询字符串参数值和表单值。这已经通过ASP.NET Core 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;
}
}
然后ConfigureServices()
在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();
});
有很多帖子建议使用属性方法。这是一个已经具有trim属性和许多其他属性的包:Dado.ComponentModel.Mutations或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核心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();
}