One of the first things we went over in my first computer science class was the idea of preconditions and postconditions for functions: what the caller should expect will be needed before and how the results will be after the call. We also discussed parameter validation, which I've found more and more to be important. Parameter validation is not only important for security purposes, but helpful in debugging scenarios when you wouldn't otherwise be sure that an exception is being generated because of calling code or internal function code (such as when you attempt to reference a null parameter).
.NET has a few argument-centric exceptions (ArgumentException, ArgumentOutOfRangeException, ArgumentNullException), as well as some more specific exceptions that could be raised when appropriate (FileNotFoundException, for instance). But it's usually somewhat cumbersome to work with them in a regular way; for each function for which you're validating parameters, you need to have code along these lines:
1: public static int DoCheckRevision(
2: string valueString,
3: string files,
4: int mpqNumber)
6: if (valueString == null)
7: throw new ArgumentNullException("valueString", Resources.crValstringNull);
8: if (files == null)
9: throw new ArgumentNullException("files", Resources.crFileListNull);
10: if (files.Length != 3)
11: throw new ArgumentOutOfRangeException("files", files, Resources.crFileListInvalid);
(Taken from MBNCSUtil - CheckRevision.cs)
What we see here is that for three parameter checks, we've got six lines of code. Kind of lame, if you ask me.
In recent projects, I've been adding to a new class, called Contract. Contract is a static class that has a series of methods that do nothing but validate parameters. This is the current function list:
- RequireInstance(object o, string paramName): Raises ArgumentNullException if o is null.
- RequireStringWithValue(string s, string paramName): Raises ArgumentNullException if s is null, and ArgumentOutOfRangeException if s is zero-length.
- RequireOpenConnection(IDbConnection con, string paramName): Raises ArgumentNullException if con is null, InvalidOperationException if con.State is not ConnectionState.Open.
- RequireBytes(byte buffer, string paramName, int exactCount): Raises ArgumentNullException if buffer is null, ArgumentOutOfRangeException if buffer.Length is not equal to the exactCount parameter.
- RequireBytes(byte buffer, string paramName, int min, int max): Raises ArgumentNullException if buffer is null, ArgumentOutOfRangeException if buffer.Length is not between min and max.
- Assert(bool test, string failureMessage): Raises InvalidOperationException if test is false.
- Assert(bool test, string failureMessage, string paramName): Raises ArgumentException if test is false.
- MaxStringLength(string s, string paramName, int maxLength): Raises ArgumentNullException if s is null, ArgumentOutOfRangeException if s is zero-length or longer than maxLength.
- RequireType(object obj, Type type, string paramName): Raises ArgumentNullException if obj is null, InvalidCastException if obj cannot be assigned to a variable of the type specified in the type parameter.
- RequireEnumValue(object value, Type type, string paramName): The same exceptions as RequireType, as well as ArgumentOutOfRangeException if value is not defined within the enumeration specified by the type parameter.
- RequireEnumFlagsValue(object value, Type type, string paramName): The same exceptions as RequireEnumFlagsValue, but supports bitwise combinations of flags defined. Somewhat slower because it uses Reflection to retrieve the enum field values.
- MaxArrayItems(Array array, string paramName, int maxItemCount): Raises ArgumentNullException if array is null, or ArgumentOutOfRangeException if the array's length is longer than maxItemCount.
All of the method calls are flagged with [DebuggerStepThrough], [DebuggerHidden], and [Conditional("DEBUG")]. That means that parameter validation will intrinsically only happen during debug builds, and you won't see where the exceptions are actually being thrown - the debugger will stop at the call to the Contract validation call, which makes it clear why exactly the exception is being thrown.
There is an additional caveat - as I have been working on this class, I added a private static method called ValidateContractParam. This method behaves like Assert, but throws an InvalidProgramException. For example, if you call Contract.RequireBytes(buffer, "buffer", 30, 20) - where max < min, an InvalidProgramException will be raised. This exception should never be caught, because it indicates you are misusing the Contract class.
Here is example code using the Contract class for my new blogging software:
1: public override void SaveUserProfile(User user, Dictionary<string, string> profile, IDbConnection connection)
3: Contract.RequireInstance(user, "user");
4: Contract.Assert(!user.IsNew, "Cannot save a user profile for a new user. Save the user first.", "user");
5: Contract.RequireInstance(profile, "profile");
6: Contract.RequireOpenConnection(connection, "connection");
In this example, it is quick and efficient to ensure that all of the state being passed into method call is exactly as it should be.
The Contract class is meant to be tailored to each program that uses it as a debugging aid. Feel free to include it in whatever programs you want; I'm releasing it to public domain. It can be downloaded from my web site.