Jeff S said:
Before I go off and paint myself into a corner by writing a bunch of code
that I later regret, I'd appreciate your feedback on my approach to
exception handling: My objective is this: in the DAL, if a runtime exception
occurs (e.g., can't connect to SQL Server), I want to log the details to an
application log, and then throw a custom exception up to the presentation
layer (Web UI in this case) that has a user-friendly message.
I defined the custom exception (which inherits from System.Exception of
course)
Best practices call for deriving it from ApplicationException; either will
work but this is the preferred approach.
as a public member of the DAL. The intent is to set the
user-friendly message in the DAL because that's where I know exactly what
went wrong and can translate that into what the user should do about it, if
anything.
Correct. I usually recommend the wrap-throw methodology, where you throw a
new exception (just as you propose) with a message that clearly describes
why the call failed, and also chain the original exception to the new
exception by setting it as the InnerException. For example...
throw new CustomApplicationException("A real friendly message
here",exRealExceptionObject);
This preserves the original exception (and stack trace) as well as providing
you a chance to add additional context information to aid the user in
determining what/where the problem is.
Separately, throwing a custom exception from the DAL (as opposed to simply
rethrowing the original exception) has the benefit that the presentation
layer (PL) can easily differentiate between different types of errors (those
originating in the DAL and those originating in the PL itself) without
having multipile try... catch blocks (would instead have multipile catch
blocks).
I'm not sure why you would need separate try-catch blocks just because you
rethrow the original exception. However, defining custom exceptions has
some benefits though I am less certain that they are really that terribly
useful when thrown across a machine (or component) boundary. You should
define a custom exception when you are adding new fields to the object or a
capability that the existing exception objects don't support, but this is
not as clear-cut as it sounds - in fact I consider this aspect of .net
programming to still be something of a black-art and is a matter of
considerable debate. This also doesn't address the issue of throwing
exceptions versus using sentinel return values to indicate an error.
If all the custom exception objects are used internally and do not propagate
outside of the boundaries of the component then there's little downside to
extending the hierarchy however you please, but if they do propagate outside
the component that generates them then this requires careful planning and
execution (read down for more..) I would not use a custom exception unless
there was a clear benefit to doing so.
Another reason to differentiate in the PL is that the PL in my case
would assume that any DAL exceptions have already been logged and would only
log non DAL exceptions. This would get me to another objective of logging
every exception and logging each only once (not once in the DAL and then
again after rethrown to the PL).
That's a nice touch but you may want to log it twice anyway. If the DAL is
on a server and the the UI is on another machine then it would be useful to
have both sides log the exception; this ensures that at least one record of
the failure exists (the SOAP reply packet may get lost, garbled, etc.). Also
you may only have access to one machine; e.g. an administrator may not be
present at or have access to the client site but can still view the error
log.
So, what do you think? Am I totally nuts? Is this allright, or would my
approach amount to being a Rube Goldberg device?
You're not nuts and it's not a hack; this is one of the recommended
procedures.
There are some additional constraints this approach requires that you should
be aware of. The most important is that the assembly that defines the custom
exception must be available on both the client and server machines,
otherwise the client will be unable to deserialize the exception object.
If the exception class is defined in an assembly with a strong name then
there are versioning issues that must be dealt with (e.g. the server throws
an exception defined in assembly 1.5 and the client has assembly 1.2). And
even if the assembly does not have a strong name to force strict binding on
you there are other reasons for making your exception object version
agnostic - you may add or remove fields and you absolutely, positively want
to be able to deserialize that object when it propagates across the wire to
your client, otherwise your nice shiny custom exception will be tossed and
instead you will be notified of a deserialization exception (note: this is
also a problem when throwing exceptions across any boundary, including
appdomains).
You should take a look at the MSFT exception management block; it may be a
good building block for you to start with.
I've condensed a list of DOs and DONTs for extending the exception hierarchy
that you may (or may not
) find useful. I'll repeat them here (use at
your own risk of course and take it all with a dose of common sense). Many
are repeated from the MSFT guidelines.
DO's
1.. Do define your own exception class when appropriate. This occurs when
you expect class library users to perform a programmatic action based on the
exception type.
2.. Create a single base application class from which all your additional
specific exceptions are derived. This base class should have fields common
to all your exceptions, such as machine name, time-date stamp, etc.
1.. When dealing with multiple products there may be value in adding a
base class from which all components derive their own types. This would
require an assembly that needs to be deployed with all products.
3.. End exception class names with the suffix Exception.
4.. Provide three constructors for your exception class.
1.. XxxException() { . }
2.. XxxException(string message){ . }
3.. XxxException(string message, Exception inner){ . }
5.. All custom exception classes defined should be remotable (see below).
6.. The exception should be annotated with the [Serializable] attribute at
the class level.
7.. If extending the custom exception class with new fields you will need
to implement the ISerializable interface to allow it to be marshaled across
remote boundaries. You will need to add the custom constructor
protected XxxException(SerializationInfo info, StreamingContext context) :
base(info, context) { . }
and you will also need to persist the custom fields by overriding the
GetObjectData method, which has the following signature:
protected GetObjectData(SerializationInfo info, StreamingContext context)
{ . }
8.. All fields used to extend the exception class should be public so that
a single logging routine, which uses reflection to traverse public
properties, can extract and log all relevant data.
9.. Provide properties for programmatic access to extra information, but
only when there is a scenario where programmatic access is useful. Otherwise
include this extra information within the descriptive text.
10.. Override the ToString() implementation so that the extra fields you
added will be easily accessible.
Don'ts
1.. Don't extend classes directly from the base System.Exception class.
2.. Don't extend the hierarchy if the exception cannot be used to
programmatically recover from or perform a unique custom action based on the
exception type. In other words, do not do this just for informational
purposes - use the message field for that.
3.. Don't use exceptions for normal or expected errors - use a return
value or outbound argument for these.
Dave