The .NET framework provides a few exceptions for use by applications, they are,
These exception types are very useful and cover most cases. But they are usually not enough.
Let's say you have some special validation where arguments to a method have to 'jive'. Such as a ChangePassword method on a User object. The ChangePassword method takes maybe three arguments, oldPassword, newPassword and verifyNewPassword. The newPassword and verifyNewPassword arguments must match. You could just throw an ArgumentException and populate the message appropriately. This causes a potential issue since by raising a generic exception type you are forcing the caller to handle the exception in a generic way. If the caller wants to do something special for this error, it has to parse the error message. Not very nice.
The structured programming world employs error codes to relay the exception type. You could implement this in C# by defining a special exception type extended from Exception and include an integer ErrorCode property. Then assign every specific exception type a number using a series of enum types defined in the custom exception class. Use enumeration types to logically group exception types, and reduce the need to reserve blocks of numbers.
The error code solution works. However, imagine what the handler looks like from the caller's point of view. There is probably a switch statement of some kind. Not a very Object Oriented construct. In and Object Oriented design switch statements on enum codes do not belong. See Replace Type code with subclass. Enter Custom Exception Classes.
Custom Exception Classes
If you have error type codes, and you have switching logic on those codes, applying 'Replace Type Code with Subclass' will lead you down the path of custom exceptions. This essentially means creating a catalog of specialized exceptions derived from the Exception class in the System namespace.
Deciding what exception classes to create can be difficult. Over specialized classes could result in a huge and unwieldy group of exceptions. Under specialized classes are essentially meaningless. As in all object modelling, you identify classes to represent concepts in the domain. Exceptions are no exception(!) to this rule. Ask, what error condition concept you are trying to communicate. You might create an exception class to represent violation of an object relationship, or an exception to indicate an attempt to create a duplicate object. These classes would most likely include information regarding the objects involved, so as much information can be relayed to the user interface. Depending on the situation your exception classes may be more specific. For example, you may wish to express certain types of object relationship violation errors.
Attempt to model your exceptions based on the current need. Don't attempt to satisfy all possible scenarios. If you know how the caller will deal with the exception, model to that requirement. Extend later. If you find yourself tempted to add error numbers and switch on them - refactor to create exception classes.
It is somewhat painful to create exception classes properly. Run FxCop against your assembly and you'll see what I mean. If you are creating many exception classes it would be wise to create a codegen macro in VS, or at least have some code to copy/paste from to make it easier. You may also find yourself creating exception classes that add nothing. No additional data or methods, just a new type. I think this is OK, so long as you have catch statements for those exception types.