C# 中参数验证方式
一般在写方法的时候,第一步就是进行参数验证,这也体现了编码者的细心和缜密,但是在很多时候这个过程很枯燥和乏味,比如在拿到一个API设计文档的时候,通常会规定类型参数是否允许为空,如果是字符可能有长度限制,如果是整数可能需要判断范围,如果是一些特殊的类型比如电话号码,邮件地址等,可能需要使用正则表达式进行判断。
通常,我们一般都是在方法开始的地方进行条件判断,然后抛出合适的异常,这是最普通和通用的做法,但是在.NET中,利用一些语言特性和类库,可以使用一些其他的方式将我们从复杂繁琐的工作中解放出来。本文逐一介绍能够用来进行参数验证的方式,他们包括直接判断语句,帮助类,扩展方法,Customer Attribute,Enterprise Liberary,Debug.Assert,Code Contract等。可以看到在.NET中随着版本的演化,逐步添加了很多声明式编程(Declarative programming)的风格,这样的代码会直接表明what而不是how,从而使得代码更加清晰和易维护。
现在来看下这些参数验证的方法。
一 一般的方法
假设我们有一个方法如下,用来进行登记注册,需要传入姓名和年龄。
public bool Register(string name, int age)
{
//insert into db
}
当然,不是传进什么东西都能调用我们的方法。一般地,我们首先要做的是,对参数进行合法性验证,比如如果要参加上海国际马拉松比赛,需要年龄大于10岁小于70岁。一般的验证方法如下:
public bool Register(string name, int age)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("name should not be empty", "name");
}
if (age < 10 || age > 70)
{
throw new ArgumentException("the age must between 10 and 70","age");
}
//insert into db
}
我们会发现,如果每一个方法都这样判断的话,非常麻烦和繁琐。于是就想到把他提取到一个帮助方法中。
public static class ArgumentHelper
{
public static void RequireRange(int value, int minValue, int maxValue, string argumentName)
{
if (value > maxValue || value < minValue)
{
throw new ArgumentException(string.Format("The value must be between {0} and {1}", minValue, maxValue),
argumentName);
}
} public static void RequireNotNullOrEmpty(string value, string argumentName)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("The value can't be null or empty", argumentName);
}
}
}
这样,在所有需要进行区间验证和非空验证的地方,调用这个帮助类中的方法即可。
public bool Register(string name, int age)
{
ArgumentHelper.RequireNotNullOrEmpty(name,"name");
ArgumentHelper.RequireRange(age,10,70,"age");
//insert into db
}
在C#3.0 中,引入了扩展方法,因此可以以一种更优雅的方式来进行参数验证,我们将前面的帮助方法改写如下:
public static class ArgumentHelper
{
public static void RequireRange(this int value, int minValue, int maxValue, string argumentName)
{
if (value > maxValue || value < minValue)
{
throw new ArgumentException(string.Format("The value must be between {0} and {1}", minValue, maxValue),
argumentName);
}
} public static void RequireNotNullOrEmpty(this string value, string argumentName)
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("The value can't be null or empty", argumentName);
}
}
}
这样,我们的验证变成了如下:
public bool Register(string name, int age)
{
name.RequireNotNullOrEmpty("name");
age.RequireRange(10,70,"age");
//insert into db
}
有了扩展方法,就可以写出很多类似LINQ的比较流畅的验证语句来。一些类似的验证类库也提供了类似功能。如FluentValidation,CuttingEdge.Conditions等。比如这里取自CuttingEdge.Condition 里面的例子。
public ICollection GetData(Nullable<int> id, string xml, IEnumerable<int> col)
{
// Check all preconditions:
Condition.Requires(id, "id")
.IsNotNull() // throws ArgumentNullException on failure
.IsInRange(1, 999) // ArgumentOutOfRangeException on failure
.IsNotEqualTo(128); // throws ArgumentException on failure Condition.Requires(xml, "xml")
.StartsWith("<data>") // throws ArgumentException on failure
.EndsWith("</data>") // throws ArgumentException on failure
.Evaluate(xml.Contains("abc") || xml.Contains("cba")); // arg ex Condition.Requires(col, "col")
.IsNotNull() // throws ArgumentNullException on failure
.IsEmpty() // throws ArgumentException on failure
.Evaluate(c => c.Contains(id.Value) || c.Contains(0)); // arg ex // Do some work // Example: Call a method that should not return null
object result = BuildResults(xml, col); // Check all postconditions:
Condition.Ensures(result, "result")
.IsOfType(typeof(ICollection)); // throws PostconditionException on failure return (ICollection)result;
}
利用扩展方法也可以写出如下图中这种比较搞笑的语句。
二 使用类库或者框架
除了自己写方法之外,一些类库和框架也提供了参数验证的模块。
Enterprise Liberary
微软企业库(Enterprise Liberary)中提供了一个名为Validation Application Block的组件,专门用来验证。安装之后,运行EntLibConfig.exe 就可以使用界面的方式来添加验证
还是以前面的代码为例子。我们将name和age封装为一个名为Person的类的字段,然后使用企业库来进行验证。允许EntLibConfig.exe,加载我们编译好的dll或者exe,然后选择需要验证的字段或者方法,然后添加合适的验证规则,如下图:
完成之后,保存为app.config文件,该文件内容如下:
<configuration>
<configSections>
<section name="validation" type="Microsoft.Practices.EnterpriseLibrary.Validation.Configuration.ValidationSettings, Microsoft.Practices.EnterpriseLibrary.Validation, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="true" />
</configSections>
<validation>
<type name="ValidationConsole.Program+Person" defaultRuleset="Validation Ruleset"
assemblyName="ValidationConsole, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<ruleset name="Validation Ruleset">
<properties>
<property name="Name">
<validator type="Microsoft.Practices.EnterpriseLibrary.Validation.Validators.NotNullValidator, Microsoft.Practices.EnterpriseLibrary.Validation, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
messageTemplate="姓名不能为空" name="Not Null Validator" />
</property>
<property name="Age">
<validator type="Microsoft.Practices.EnterpriseLibrary.Validation.Validators.RangeValidator, Microsoft.Practices.EnterpriseLibrary.Validation, Version=5.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
culture="zh-CN" lowerBound="10" lowerBoundType="Exclusive"
upperBound="70" messageTemplate="Age should between 10 and 70 "
name="Range Validator" />
</property>
</properties>
</ruleset>
</type>
</validation>
</configuration>
可以看到企业库实际上是生成一个app.config文件,然后在文件中写入了参数的验证条件,然后在运行的时候引用企业库的相关dll进行验证。参数验证是可以配置的,具体的使用方法如下:
public class Person
{
public string Name { get; set; }
public int Age { get; set; } public Person(string name, int age)
{
this.Name = name;
this.Age = age;
}
}
static void Main(string[] args)
{
Validator<Person> customerValidator = ValidationFactory.CreateValidator<Person>("Validation Ruleset");
Person myCustomer = new Person("admin",9); ValidationResults validateResult = customerValidator.Validate(myCustomer); if (!validateResult.IsValid)
{
for (int i = 0; i < validateResult.Count; i++)
{
Console.WriteLine("验证出错 {0}:" + validateResult.ElementAt(i).Message, i + 1);
}
}
else
{
Console.WriteLine("正确");
}
Console.ReadKey();
}
运行结果如下:
ASP.NET MVC
还可以利用自定义属性(Customer Attribute)来进行参数验证,ASP.NET MVC 的Model中就是使用数据标记(Data Annotations)这种属性来进行验证。Data Annotations其实是一系列继承自Attribute的可以用在类或者类的属性上的自定义属性类。
System.ComponentModel.DataAnnotations 程序集中内建有一些诸如 Required, Range, RegularExpression and StringLength的类, 这些自定义属性类都继承自ValidationAttribute抽象类:
public abstract class ValidationAttribute : Attribute
{
public string ErrorMessage { get; set; } public virtual bool IsValid(object value); protected virtual ValidationResult IsValid(object value, ValidationContext
validationContext); // other members
}
如果在ASP.NET MVC 中,我们的Person Model就可以写成如下:
public class Person
{
[Required]
public string Name { get; set; }
[Range(10, 70)]
public int Age { get; set; }
}
很简洁,在编写实体类的时候,就可以顺便把验证规则给定了。这样,我们在实例化Person类,然后调用框架中相应的方法,如果不满足条件,就会抛出相应的异常。
PostSharp
一些商业软件,更是将利用属性进行验证做到了极致,比如PostSharp这款商业软件。下面是该网站的宣传页:
可以看到,在方法的参数中,可以在前面使用自定义属性来标记,然后在系统运行的时候进行动态的验证。
PostSharp使用的是一种所谓静态注入的方式,也就是在编译好的程序集中的类型或者某个方法里注入IL代码,是在代码编译的时候,而不是在运行时注入的。Visual Studio通过MSBuild来执行生成过程,PostSharp是把自己作为一系列的Build Task来插入到代码生成过程中来的。其原理可以参看 .NET下的AOP: PostSharp 原理分析 这篇文章。这里引用了文中的一幅图,很形象的说明了PostSharp的原理:
自己动手
其实使用属性来进行验证很简单,我们也可以自己动手来实现类似PostSharp的功能,当然,在使用Customer Attribute之前,首先您需要了解Attribute这个类, 中文的话,您可以参考CSDN上的Attribute在.net编程中的应用这一些列6篇文章。下面就介绍如何实现PostSharp中的使用自定义属性对参数进行标记验证。其实您看过ASP.NET MVC 中的System.ComponentModel.DataAnnotations应该就可以知道该怎么实现了。
首先,新建一个ArgumentValidationAttribute抽象类。因为按照约定,所有继承自Attribute的类名称后面必须带有Attribute。这个类中只有一个抽象方法Validate,用来验证。
public abstract class ArgumentValidationAttribute:Attribute
{
public abstract void Validate(object value, string argumentName);
}
然后,我们定义一个用来验证非空的自定义属性NotNullAttribute,注意到在该类上,我们使用了AttributeUsage属性,在其构造函数参数中,我们传入了 AttributeTargets.Parameter 这个枚举,表明该标记只能用在方法的参数上。
[AttributeUsage(AttributeTargets.Parameter)]
public class NotNullAttribute : ArgumentValidationAttribute
{
public override void Validate(object value, string argumentName)
{
if (value == null)
throw new ArgumentNullException(argumentName);
}
}
然后定义了一个用来验证范围的InRangeAttribute,这里我们定义了一个构造函数,使得可以传入一个区间范围。
[AttributeUsage(AttributeTargets.Parameter)]
public class InRangeAttribute : ArgumentValidationAttribute
{
private int min;
private int max; public InRangeAttribute(int min, int max)
{
this.min = min;
this.max = max;
} public override void Validate(object value, string argumentName)
{
int intValue = (int)value;
if (intValue < min || intValue > max)
{
throw new ArgumentOutOfRangeException(argumentName,string.Format("min={0},max={1}",min,max));
}
}
}
有了上面两个类,我们还需要在一个大的框架类验证调用这些验证方法,通常,我们会使用诸如接口注入的方式来实现。这里仅列出关键部分。
public class ValidationInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
ParameterInfo[] parameters = invocation.Method.GetParameters();
for (int index = 0; index < parameters.Length; index++)
{
var paramInfo = parameters[index];
var attributes = paramInfo.GetCustomAttributes(typeof(ArgumentValidationAttribute), false); if (attributes.Length == 0)
continue; foreach (ArgumentValidationAttribute attr in attributes)
{
attr.Validate(invocation.Arguments[index], paramInfo.Name);
}
} invocation.Proceed();
}
}
然后,再回头看我们的代码,我们首先抽象一个IRegister接口:
public interface IRegister
{
void Register([NotNull] string name, [InRange(10, 70)] int age);
}
可以看到,现在接口中的方法,参数前面已经可以写我们之前定义的用于验证功能的属性了,接口方法中定义了参数的验证规则之后,所有实现该接口的方法中就不需要再次定义了。我们的注册类只需要实现该接口,然后再执行Register之前,统一验证即可。
public class ShMarathon : IRegister
{ public void Register(string name, int age)
{
//doesn't need to validate
//insert into db
}
}
这种方式其实是AOP(面向方面编程)的一种思想,更多的资料您可以参考AOP in .NET: Practical Aspect-Oriented Programming和Dependency Injection in .NET
三 Code Contract
Code Contracts 是微软研究院开发的一个编程类库,我最早看到是在C# In Depth 的第二版中,当时.NET 4.0还没有出来,当时是作为一个第三方类库存在的,到了.NET 4.0之后,已经加入到了.NET BCL中,该类存在于System.Diagnostics.Contracts 这个命名空间中。
namespace System.Diagnostics.Contracts
{
// Summary:
// Contains static methods for representing program contracts such as preconditions,
// postconditions, and object invariants.
public static class Contract
{
public static event EventHandler<ContractFailedEventArgs> ContractFailed;
public static void Assert(bool condition);
public static void Assert(bool condition, string userMessage);
public static void Assume(bool condition);
public static void Assume(bool condition, string userMessage);
public static void EndContractBlock();
public static void Ensures(bool condition);
public static void Ensures(bool condition, string userMessage);
public static void EnsuresOnThrow<TException>(bool condition) where TException : Exception;
public static void EnsuresOnThrow<TException>(bool condition, string userMessage) where TException : Exception;
public static bool Exists<T>(IEnumerable<T> collection, Predicate<T> predicate);
public static bool Exists(int fromInclusive, int toExclusive, Predicate<int> predicate);
public static bool ForAll<T>(IEnumerable<T> collection, Predicate<T> predicate);
public static bool ForAll(int fromInclusive, int toExclusive, Predicate<int> predicate);
public static void Invariant(bool condition);
public static void Invariant(bool condition, string userMessage);
public static T OldValue<T>(T value);
public static void Requires<TException>(bool condition) where TException : Exception;
public static void Requires(bool condition);
public static void Requires(bool condition, string userMessage);
public static void Requires<TException>(bool condition, string userMessage) where TException : Exception;
public static T Result<T>();
public static T ValueAtReturn<T>(out T value);
}
}
Code Contract 使得.NET 中契约式设计和编程变得更加容易,Contract中的这些静态方法方法包括
- Requires:函数入口处必须满足的条件
- Ensures:函数出口处必须满足的条件
- Invariants:所有成员函数出口处都必须满足的条件
- Assertions:在某一点必须满足的条件
- Assumptions:在某一点必然满足的条件,用来减少不必要的警告信息
Code Contract 的使用文档您可以从官网下载到。为了方便使用Visual Studio开发。我们可以安装一个Code Contracts Editor Extensions插件。安装完了之后,点击Visual Studio中的项目属性,可以看到如下丰富的选择项:
现在,在我们的Register方法中,可以使用Contract来进行判断,使用方式和Debug.Assert类似:
public static bool Register(string name, int age)
{
Contract.Requires(!String.IsNullOrEmpty(name));
Contract.Requires(age > 10 && age < 70);
//insert into db
return false;
}
当我们传入错误的age值的时候,就会报错:
Contract和Debug.Assert有些地方相似:
- 都提供了运行时支持:这些Contracts都是可以被运行的,并且一旦条件不被满足,会弹出类似Assert的一样的对话框报错,如下:
- 都可以在随意的在代码中关闭打开。
但是Contract有更多和更强大的功能:
- Contracts的意图更加清晰,通过不同的Requires/Ensures等等调用,代表不同类型的条件,比单纯的Assert更容易理解和进行自动分析
- Contracts的位置更加统一,将3种不同条件都放在代码的开始处,而非散见在函数的开头和结尾,便于查找和分析。
- 不同的开发人员、不同的小组、不同的公司、不同的库可能都会有自己的Assert,这就大大增加了自动分析的难度,也不利于开发人员编写代码。而Contracts直接被.NET 4.0支持,是统一的。
- 它提供了静态分析支持,这个我们可以通过配置面板看到,通过静态分析Contracts,静态分析工具可以比较容易掌握函数的各种有关信息,甚至可以作为Intellisense
Contract中包含了三个工具:
- ccrewrite, 通过向程序集中些如二进制数据,来支持运行时检测
- cccheck, 运行时检测
- ccdoc, 将Contract自动生成XML文档
下图是Contract的原理,图片来自 .NET 4.0中的新功能介绍:契约式设计 (Design By Contracts) 这篇文章, 这也是为什么要比Debug.Assert强大的原因,我们只需要将所有的执行前判断和执行后判断的条件,写到一个地方,然后再编译代码的时候,ccrewrite 会帮我们生成相应的代码,保存起始值,并将相应的代码插入到方法的合适位置。使得我们的代码更加整洁和容易维护。
四 总结
本文简单介绍了在.NET 中用来进行方法参数验证的各种方式,包括传统的在方法执行前编写判断语句,提取到公共帮助类中,使用扩展方法,以及一些类库如Enterprise Liberary,PostSharp,ASP.NET MVC然后实现了一个简单的利用自定义属性来进行方法参数验证的例子,最后介绍了一下.NET 4.0种的Code Contract,在开发中这些验证方式能够统一我们的方法参数验证,在一定的程序上可以减少工作量,希望本文对您有所帮助。
C# 中参数验证方式的更多相关文章
-
C# 中参数验证方式的演变
一般在写方法的时候,第一步就是进行参数验证,这也体现了编码者的细心和缜密,但是在很多时候这个过程很枯燥和乏味,比如在拿到一个API设计文档的时候,通常会规定类型参数是否允许为空,如果是字符可能有长度限 ...
-
Springboot统一参数验证方式
Springboot统一验证方式 在提供http api 接口形式的服务中,通过都会传递参数为一个对象.我们需要对这个对象的各个字段进行校验.来判断是否为合法值. 传统的方式为自己获取每个字段的值,自 ...
-
ASp.net中Froms验证方式
微软的ASP.NET提供了3种用户验证方式,即Windows验证.窗体(Forms)验证和护照验证(Passport)验证. 由于验证方式各不相同,因而这3种验证方式在使用范围上也有很大的不同, Wi ...
-
Web验证方式(1)--Http Basic Authentication
Http Basic Authentication是HTTP协议中定义的Web系统中的验证方式.参考wiki 主要的实现机制如下: 1. 用户通过浏览器匿名访问web资源. 2. web服务器检测到w ...
-
Azure Service Bus 中的身份验证方式 Shared Access Signature
var appInsights=window.appInsights||function(config){ function r(config){t[config]=function(){var i= ...
-
SpringBoot08 请求方式、参数获取注解、参数验证、前后台属性名不一致问题、自定义参数验证注解、BeanUtils的使用
1 请求方式 在定义一个Rest接口时通常会利用GET.POST.PUT.DELETE来实现数据的增删改查:这几种方式有的需要传递参数,后台开发人员必须对接收到的参数进行参数验证来确保程序的健壮性 1 ...
-
Java中的参数验证(非Spring版)
1. Java中的参数验证(非Spring版) 1.1. 前言 为什么我总遇到这种非正常问题,我们知道很多时候我们的参数校验都是放在controller层的传入参数进行校验,我们常用的校验方式就是引入 ...
-
SpringMVC中使用@Valid和BindingResult进行参数验证
我们知道,后端Controller层一般是第一层被调用,它用来接收参数以及转发,那么参数的校验也就在这一层,例如非空和格式校验等等. 手动验证 public String validPhone(Str ...
-
再说表单验证,在Web Api中使用ModelState进行接口参数验证
写在前面 上篇文章中说到了表单验证的问题,然后尝试了一下用扩展方法实现链式编程,评论区大家讨论的非常激烈也推荐了一些很强大的验证插件.其中一位园友提到了说可以使用MVC的ModelState,因为之前 ...
随机推荐
-
eclipse 粘贴字符串自动添加转义符
eclipse -> Window -> Preferences -> Java -> Editor -> Typing -> 勾选{Escape text whe ...
-
HTTP方法:GET对比POST
http://blog.csdn.net/zhenyu5211314/article/details/17067817
-
tnsnames.ora 监听配置文件详解
今天是重要的一天.将XML数据导入ORACLE遇到很多问题,学了好多,其中很长时间花在网络配置上,刚开始学,具体原因不知道,先把搜集到的好文章存下来,以后慢慢研究. 监听配置文件 ...
-
DuiLib 自定义识别控件
遇到一个断点无法识别自定义的控件,运气比较好,一搜就搜出来了: 参考地址:http://www.bkjia.com/ASPjc/992050.html 主要是这个函数: CControlUI* CDi ...
-
[转]如何让div中的内容垂直居中
转自:http://blog.163.com/yan_1990/blog/static/197805107201211311515454/ 虽然Div布局已经基本上取代了表格布局,但表格布局和Div布 ...
-
nyoj-----284坦克大战(带权值的图搜索)
坦克大战 时间限制:1000 ms | 内存限制:65535 KB 难度:3 描述 Many of us had played the game "Battle city" ...
-
1、vs2012 mvc3项目ObjectContext类找不到的问题
在vs2012下找不到ObjectContext类,取而代之的是DBContext,实体对象的Attach方法,上下文的ObjectStateManager对象都找不到,解决办法: 在设计视图中打开E ...
-
js实现导航菜单栏随着屏幕的滚动进行滚动的效果
$(window).scroll(function () { var $nav = $(".floatingMenu ul li"), length = $nav.length-1 ...
-
解决Centos crontab没有按时运行的问题
我装了centos,用一点一直无法搞定,就是定时关机.我只是想做这一件事: 每天凌晨1点自动关机 0 1 * * * shutdown now -h 然而,无论我如何设置,都是失败. 每当我睡了一觉, ...
-
【转载】 大龄码农那些事——也谈996.ICU
原文地址: https://www.cnblogs.com/helloyaren/p/10657414.html 请扫码关注!!! 您的关注将是您做的最正确的事情!!! 大龄码农那些事专注分享大龄码农 ...