Warning: the following code has become obsolete since the release of Castle RC3. I'll update as soon as I can. The concepts still apply, though.
Recently I've been writing an app using Castle Windsor. While I was writing the service classes, the following problem came up: I don't want my services' consumers catching Dao exceptions. I'm using ActiveRecord to persist domain objects, but it could have been directly NHibernate, or iBatis. Either way, whoever uses the service classes should be ignorant about persistance details. Suppose the service interface looks like this:
public interface ISomeService { void ProvideService(); }and its implementation lets an internal exception pass, which we could model like this to make things simple:
public class SomeService : ISomeService { public void ProvideService() { throw new InternalException(); } } public class InternalException : ApplicationException {}Now we'd like to translate InternalException into an ExternalException, which is a service-level exception. We could manually catch InternalException in every method of SomeService, but that's annoying.
Let's use Windsor instead. We need three components: the exception translator (which says what external exception corresponds to each internal exception), the interceptor (which actually does the try..catch) and a contributor (which installs the interceptor and translator).
First, let's write a test that should pass when we finish installing all three components:
[TestFixture] public class ExceptionTranslatorTests { [Test] [ExpectedException(typeof (ExternalException), "1")] public void ExceptionIsTranslated() { IWindsorContainer container = new WindsorContainer(); container.AddComponent("service", typeof (ISomeService), typeof (SomeService)); ISomeService svc = container.Resolve<ISomeService>(); svc.ProvideService(); } } public class ExternalException : ApplicationException { public ExternalException(string message) : base(message) {} public ExternalException() {} public ExternalException(string message, Exception innerException) : base(message, innerException) {} }If we run this test now, it will fail, since the exception thrown by ProvideService() is still InternalException instead of the expected ExternalException.
Now let's build the translator:
public interface IExceptionTranslator { Exception translate(Exception e); } public class SomeExceptionTranslator : IExceptionTranslator { public Exception translate(Exception e) { return new ExternalException("1", e); } }Simple, no? Just make sure you wrap the original exception, so you don't lose stack information and stuff.
Now for the generic interceptor:
public class ExceptionTranslationInterceptor<T> : IMethodInterceptor where T : IExceptionTranslator { private T translator; public ExceptionTranslationInterceptor(T translator) { this.translator = translator; } public object Intercept(IMethodInvocation invocation, params object[] args) { try { return invocation.Proceed(args); } catch (Exception e) { throw translator.translate(e); } } }Notice that the actual translation logic will be injected by Windsor into the interceptor. Oh, and make sure theIMethodInterceptor you implement is the one in Castle.Core.Interceptor, not the one in AopAlliance.Intercept.
You may wonder why the interceptor takes <T>, you'll see why in a second when we build the contributor:
public class ExceptionTranslatorContributor<T, T2> : IContributeComponentModelConstruction where T2 : IExceptionTranslator { public void ProcessModel(IKernel kernel, ComponentModel model) { if (typeof (T).IsAssignableFrom(model.Implementation)) { kernel.AddComponent(typeof (T2).ToString(), typeof (T2)); kernel.AddComponent(string.Format("ExceptionTranslationInterceptor<{0}>", typeof(T2)), typeof(ExceptionTranslationInterceptor<T2>)); model.Interceptors.Add(new InterceptorReference(typeof (ExceptionTranslationInterceptor<T2>))); } } }This contributor takes two types: T, which is the service to intercept, and T2, which is the IExceptionTranslator. It installs the translator and the corresponding interceptor as components, then it installs the interceptor as such. By making the interceptor and the contributor generic, we can easily associate different translators for different services, just by setting up the contributor in the container, like this:
[TestFixture] public class ExceptionTranslatorTests { [Test] [ExpectedException(typeof (ExternalException), "1")] public void ExceptionIsTranslated() { IWindsorContainer container = new WindsorContainer(); container.Kernel.ComponentModelBuilder.AddContributor(new ExceptionTranslatorContributor<SomeService, SomeExceptionTranslator>()); container.AddComponent("service", typeof (ISomeService), typeof (SomeService)); ISomeService svc = container.Resolve<ISomeService>(); svc.ProvideService(); } }And now the test passes!
Full code shown here is available here.
No comments:
Post a Comment