This article will present a few .NET 2.0 helper classes for common tasks such as validating arguments and raising events. I will show you how these helper classes work and how they can make your code easier to read, thereby increasing maintainability. The source download contains all three fully-documented helper classes, along with a separate project containing unit tests. The Visual Studio 2005 project files are also included, but you can certainly build the code in your .NET 2.0 environment of choice.
I recently created these classes whilst working on some personal projects. They are by no means groundbreaking, but they will help to reduce your coding effort and help you avoid some common coding errors.
The first helper class deals with validating method arguments. Suppose you are implementing the following simple method:
public static string ChangeCase(string val, Casing casing)
{
if (casing == Casing.Upper)
{
return val.ToUpper();
}
else
{
return val.ToLower();
}
}
All this method does is convert a string to upper or lower case, depending on the value of casing. There are two problems with this method. First, what happens if val is null? Second, what happens if casing is not a valid member of the Casing enumeration? To illustrate the first problem:
string result = ChangeCase(null, Casing.Upper);
Here, we are passing in a null value, which will cause our method to fail with an unhelpful message:
System.NullReferenceException was unhandled
Message="Object reference not set to an instance of an object."
Of course, we have the source code so we can track down the problem via the stack trace. However, if you didn't have the source code � or if the application in question is large � tracking down little issues like this can be very time-consuming. Now consider this:
string result = ChangeCase("MyString", (Casing) 23);
Assume that Casing is defined as:
public enum Casing
{
Upper,
Lower
}
23 is therefore an invalid value, but this invocation will not fail. Instead, it will fall through the if statement and return "mystring." In most cases, this is probably unintended and not what you want. The traditional way of fixing these issues might look something like this:
public static string ChangeCase(string val, Casing casing)
{
if (val == null)
{
throw new ArgumentNullException("val");
}
if (!Enum.IsDefined(typeof(Casing), casing))
{
throw new ArgumentException("casing");
}
if (casing == Casing.Upper)
{
return val.ToUpper();
}
else
{
return val.ToLower();
}
}
The problem with this approach is that it quickly becomes tedious and is particularly painful for methods with a large number of arguments. Enter, ArgumentHelper:
public static class ArgumentHelper
{
public static void AssertEnumMember<TEnum>(TEnum enumValue,
string argName) where TEnum: struct, IConvertible;
public static void AssertEnumMember<TEnum>(TEnum enumValue,
string argName, params TEnum[] validValues) where TEnum: struct,
IConvertible;
public static void AssertGenericArgumentNotNull<T>(T arg,string argName);
public static void AssertNotNull<T>(T arg,string argName) where T: class;
public static void AssertNotNull<T>(T? arg,
string argName) where T: struct;
public static void AssertNotNull<T>(IEnumerable<T> arg, string argName,
bool assertContentsNotNull);
public static void AssertNotNullOrEmpty(string arg, string argName);
public static void AssertNotNullOrEmpty(string arg, string argName,
bool trim);
}
This class provides several useful methods that we can use to validate our arguments. Here's how we could simplify the ChangeCase() method:
public static string ChangeCase(string val, Casing casing)
{
ArgumentHelper.AssertNotNull(val, "val");
ArgumentHelper.AssertEnumMember(casing, "casing");
if (casing == Casing.Upper)
{
return val.ToUpper();
}
else
{
return val.ToLower();
}
}
The first check ensures that val is not null. If it is, ArgumentNullException is thrown. The second check ensures that casing is a valid member of the Casing enumeration. If it isn't, ArgumentException with a helpful message is thrown. All of the AssertNotNull() methods can be used to ensure that arguments are not null, whether they be standard reference types, nullable value types or even collections. In the case of collections, you can optionally assert that all members of the collection are non-null.
AssertNotNullOrEmpty overloads are special cases for string arguments. As their name suggests, they will ensure that a string argument is non-null and non-empty, optionally trimming the string before performing the empty check. Thanks to JohnDeHope3, who suggested that this feature be added to the ArgumentHelper class in a thread below this article.
The AssertEnumMember() overloads can be used to check enumeration arguments. They use generic constraints to ensure that the value passed in is a value type and implements IConvertible, which all enumerations do. Unfortunately, it is not possible in .NET 2.0 to further refine these constraints to require an enum subclass. However, if the supplied type is not a valid enumeration, an appropriate exception will be thrown.
Note that the AssertEnumMember() overload used in the above example code is relatively slow. This is because it uses reflection to determine the valid values in the enumeration via the Enum.GetValues() method. To this end, there exists an overload that allows you to specify the allowable enumeration values. You should prefer this overload for two reasons:
TestPerf(). You can run this unit test to see the performance difference between these overloads. I would also like to point out that AssertEnumMember() overloads work both with flag enumerations -- i.e. enumerations decorated with the [Flags] attribute -- and non-flag enumerations. In the case of flag enumerations, the method allows a combination of flags that you specify.
Finally, you may have noticed the AssertGenericArgumentNotNull() method. This method exists for the cases where your argument's type is generic and you do not know whether it is a reference type, nullable value type or non-nullable value type. For example, consider the following code:
public void Add<T>(T item)
{
ArgumentHelper.AssertGenericArgumentNotNull(item, "item");
//item cannot be null here
}
This code will ensure that the item parameter is not null, regardless of whether it is a reference type or nullable value type. If it is a value type, the call is effectively a no-op.
The designers of .NET did a fantastic job in the area of delegates and events. They improved on it even more in .NET 2.0. However, raising events can still be a little tedious and error-prone:
protected virtual void OnMyEvent(MyEventArgs e)
{
MyEventHandler handler = MyEvent;
if (handler != null)
{
handler(this, e);
}
}
As you know, we have to ensure that MyEvent has listeners before we raise it. Otherwise, we'll get NullReferenceException when attempting to dereference its delegate. In addition, we have to do so in a thread-safe manner. That is the purpose of the local handler variable. It ensures that the null check and actual delegate invocation are not adversely affected by another thread removing the last handler from a delegate. Consider what would happen if another thread removed the last listener from the event between obtaining the delegate reference and invoking it. Say "hello" to my little friend, EventHelper:
public static class EventHelper
{
public static void BeginRaise(EventHandler handler, object sender,
AsyncCallback callback, object asyncState);
public static void BeginRaise(Delegate handler, object sender,
EventArgs e, AsyncCallback callback, object asyncState);
public static void BeginRaise<T>(EventHandler<T> handler, object sender,
T e, AsyncCallback callback, object asyncState) where T: EventArgs;
public static void BeginRaise<T>(EventHandler<T> handler, object sender,
CreateEventArguments<T> createEventArguments, AsyncCallback callback,
object asyncState) where T: EventArgs;
public static void Raise(EventHandler handler, object sender);
public static void Raise<T>(EventHandler<T> handler, object sender,
T e) where
T: EventArgs;
public static void Raise(Delegate handler, object sender, EventArgs e);
public static void Raise<T>(EventHandler<T> handler, object sender,
CreateEventArguments<T> createEventArguments) where T: EventArgs;
public delegate T CreateEventArguments<T>() where T: EventArgs;
}
This class supports the raising of events, both generic and non-generic delegates, either synchronously or asynchronously. We can use it as follows:
protected virtual void OnMyEvent()
{
EventHelper.Raise(MyEvent, this, new MyEventArgs(foo, bar));
}
We no longer have to check against null; we let the EventHelper.Raise() method do that. In addition, we get thread safety for free, since the delegate is passed in as an argument. You can see this thread safety verified in the unit test called TestThreadSafety(). Raising events asynchronously can be done via the BeginRaise() overloads. They are similar to the Raise() overloads except that they take optional callback and state arguments, as per the standard .NET asynchronous call pattern.
Those with a keen .NET eye may notice a potential problem with the above code. If there are no listeners, the event arguments instance, MyEventArgs, is still created needlessly. This would not normally be a problem, but it might be in circumstances where the event fires frequently and rarely has any listeners. To overcome this, there is an overload of Raise(), which does not create the event arguments unless absolutely necessary. It is used as follows:
protected virtual void OnMyEvent()
{
EventHelper.Raise(MyEvent, this, delegate
{
return new MyEventArgs(foo, bar);
});
}
With this overload, EventHelper will call back via the provided delegate if it needs an instance of the event arguments class. The introduction of anonymous delegates in .NET 2.0 keeps this nice and compact. You should note, however, that this overload would only be truly useful in performance-critical code paths. Moreover, it's debatable whether using this overload is simpler than writing the code manually.
Be sure to profile any code that uses this overload because you may find that the creation of the delegate instance outweighs the creation of the event arguments. There is a good discussion in the comments below started by punkrock. Please read that for more information. The EventHelper class also includes some diagnostic output when built in a Debug configuration. This information can be used to track which methods are being called when events are raised. It will also output any exceptions raised by those handlers. Note that this diagnostic information is not output in a Release configuration and no performance cost is incurred.
This final helper class I'd like to share with you, ExceptionHelper, is perhaps the most complex and lengthy. The goal of this helper class is to simplify the process of throwing exceptions and maintaining consistent and helpful messages for those exceptions. I should point out that this helper aims to be efficient in the common case -- i.e. no exception thrown -- but will be quite inefficient in the uncommon case where an exception is thrown. I don't consider this to be a problem because if you follow the best practices with exceptions, your code will only throw in an exceptional circumstance.
In addition, you may provide another API to determine beforehand whether an operation will succeed without throwing. Cases in point are the Int32.Parse() and Int32.TryParse() methods. With those thoughts aside, here are the APIs for the ExceptionHelper class:
public static class ExceptionHelper
{
public static void Throw(string exceptionKey,
params object[] messageArgs);
public static void Throw(string exceptionKey,
object[] constructorArgs,
Exception innerException);
public static void Throw(string exceptionKey,
Exception innerException,
params object[] messageArgs);
public static void Throw(string exceptionKey,
object[] constructorArgs,
params object[] messageArgs);
public static void Throw(string exceptionKey,
object[] constructorArgs,
Exception innerException,
params object[] messageArgs);
public static void ThrowIf(bool condition,
string exceptionKey,
params object[] messageArgs);
public static void ThrowIf(bool condition,
string exceptionKey,
object[] constructorArgs,
Exception innerException);
public static void ThrowIf(bool condition,
string exceptionKey, object[] c
onstructorArgs,
params object[] messageArgs);
public static void ThrowIf(bool condition, string exceptionKey,
Exception innerException,
params object[] messageArgs);
public static void ThrowIf(bool condition, string exceptionKey,
object[] constructorArgs,
Exception innerException,
params object[] messageArgs);
}
The ExceptionHelper class works in conjunction with an embedded resource in your assembly called ExceptionHelper.xml. This file must be placed inside the Properties folder of your project, as depicted in Figure 1:
An example of the format for the ExceptionHelper.xml file follows:
<?xml version="1.0" encoding="utf-8" ?>
<exceptionHelper>
<exceptionGroup type="MyNamespace.MyType, MyAssembly">
<exception key="missing" type="System.InvalidOperationException">
I can't find the byte array.
</exception>
<exception key="tooBig" type="System.InvalidOperationException">
I found the byte array but it is too big ({0} bytes).
</exception>
</exceptionGroup>
<exceptionGroup type="MyNamespace.MyOtherType, MyAssembly">
<exception key="missing" type="MyNamespace.MyException, MyAssembly">
I can't find the string.
</exception>
</exceptionGroup>
</exceptionHelper>
Exception messages are grouped by the type that will potentially throw those exceptions. This means that you don't have to come up with unique exception keys across multiple types. As long as a key is used only once per type, you will be all right.
Notice how MyType and MyOtherType both use a key called missing. Since they are in a different exception group, these keys will not conflict. The ExceptionHelper automatically discovers the correct group based on the caller. The next thing to notice is that the tooBig message contains a parameter placeholder. You will see how to pass in parameters to messages in a minute.
The final thing I wanted to point out is that you are free to throw any exception you like, including your own. To accommodate this, ExceptionHelper makes some assumptions about how your constructors are defined. If you follow best practices when defining your exception constructors, you will not run into any trouble. If ExceptionHelper has trouble constructing your exception, you will receive an exception to that effect. Yes, ExceptionHelper will throw its own exceptions if necessary.
All right then, enough with the formalities. Let's look at some code that uses ExceptionHelper:
public void ProcessBytes()
{
byte[] bytes = GetBytes();
ExceptionHelper.ThrowIf(bytes == null, "missing");
ExceptionHelper.ThrowIf(bytes.Length > 2048, "tooBig", bytes.Length);
//process bytes now
}
As you can see, this method throws an exception under two circumstances:
byte array was returned by GetBytes(). byte array returned by GetBytes() had more than 2048 bytes in it. To throw these exceptions, it simply uses one of the ThrowIf() overloads, which takes the condition as the first parameter. The second parameter is the exception key, which is used to look up the exception details in the ExceptionHelper.xml file. The second ThrowIf() call also passes in bytes.Length as a parameter for the exception message. Recall that the exception message for the tooBig key was defined as:
I found the byte array but it is too big ({0} bytes).
This means that, for example, if the number of bytes returned by GetBytes() is 2056, the message will be:
I found the byte array but it is too big (2056 bytes).
You can have as many parameters as you like in your message. Standard .NET formatting is used under the hood, so you could define a message such as:
The specified amount ({0:c}) is way too much! Consider buying
something cheaper such as a {1}. Error code: {2:X}
Here, we are using several argument placeholders, two of which have special formatting applied to them. The ExceptionHelper class has other abilities that we haven't discussed yet. Most of these abilities are rarely used, however, so I will list them rather than cover them in detail:
Throw() overloads. This is useful when you have already determined that an exception should be thrown and you don't need to re-check a condition. C# is a modern and powerful programming language. The language and library designers have taken great care to make many tasks as easy as possible for you. I have found, however, that common rudimentary tasks such as validating arguments and raising events can be made even easier with a helper class or three. This article has shown you the implementation of such helper classes. I hope you find them as useful as I have. Again, feel free to modify or enhance them for your specific project.
ArgumentHelper's enumeration support and EventHelper's non-generic events and diagnostics, plus other improvements. ArgumentHelper, including a breaking change in AssertEnumMember methods and methods in EventHelper to raise events asynchronously. | You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||