When building NServiceBus services based on the generic host, you may need to do some stuff whenever your service starts up and shuts down. The way to do that is to create classes that implement IWantToRunAtStartup, which will be picked up by the host and registered in the container as an implementation of that interface.
When the time comes to run whoever wants to run at startup, the host does a
1 |
container.ResolveAll<IWantToRunAtStartup>() |
to get all the relevant instances (or something similar if you aren’t using Windsor…).
If, however, one or more instances cannot be instantiated due to missing dependencies, you will get no kind of warning or error whatsoever! [1. At least this is the case when using Castle Windsor – I don’t know if this is also the behavior of other IoC containers… Maybe someone can clarify this…?] This means that the service will silently ignore the fact that one or more IWantToRunAtStartups could not be instantiated and run.
In order to avoid this error, I have written a test that looks somewhat like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
[Test] public void WhoeverWantsToRunAtStartupCanActuallyRun() { var container = new WindsorContainer(); PerformTheUsualRegistration(container); var typesThatWantToRun = from type in typeof (MyService.EndpointConfiguration).Assembly.GetTypes() where typeof(IWantToRunAtStartup).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface select type; ManuallyRegister(container, typesThatWantToRun); RegisterFakeBus(container); var typesThatCouldRun = container.ResolveAll<IWantToRunAtStartup>().Select(c => c.GetType()); var typesThatCouldNotRun = typesThatWantToRun.Except(typesThatCouldRun); if (typesThatCouldNotRun.Any()) { Assert.Fail(string.Join(Environment.NewLine + Environment.NewLine, typesThatCouldNotRun.Select(t => GenerateErrorDetailsFor(t, container)).ToArray())); } } string GenerateErrorDetailsFor(Type type, IWindsorContainer container) { // first, register the class as itself if is has not already been done if (!container.Kernel.HasComponent(type)) { container.Register(Component.For(type).Named(type.name + " that wants to run")); } // next, make Windsor throw an exception with all the nasty details... var exceptionText = ""; try { container.Resolve(type); } catch (HandlerException e) { exceptionText = e.Message; } return string.Format(@"Class: {0} Reason: {1}", type.Name, exceptionText); } void PerformTheUsualRegistration(IWindsorContainer container) { container.Install(FromAssembly.Containing<MyService.EndpointConfiguration>()); } void ManuallyRegister(IWindsorContainer container, IEnumerable<Type> typesThatWantToRun) { container.Register(typesThatWantToRun .Select(t => Component.For<IWantToRunAtStartup>().ImplementedBy(t)) .ToArray()); } void RegisterFakeBus(IWindsorContainer container) { container.Register(Component.For<IBus>().Instance(MockRepository.GenerateMock<IBus>()); } |
I admit that the code is kind of clunky even though I distilled the interesting parts from some of the plumbing in our real test… moreover, our real test is iterating through all the possible configurations our container can have – one for each environment – so you can probably imagine that it’s not pretty 🙂
But who cares??? The test has proven almost infinitely useful already! Whenever something that wants to run at startup cannot run at startup, TeamCity gives us error messages like this:
1 2 3 4 5 6 7 8 9 |
Class: RunSomething Reason: Can't create component 'RunSomething that wants to run' as it has dependencies to be satisfied. RunSomething that wants to run is waiting for the following dependencies: Services: - OneOfOurProjects.Api.ISomethingElse which was not registered. |