In the previous post I showed how easy it is to install an IoC container at the system boundary of your ASP.NET MVC application and have it resolve everything from there.
But what I did not show, was where the container came from – in the example, the container just pops out of nowhere on this line:
1 |
static IContainer myContainer = GetContainerInstanceFromSomewhere(); |
So how can we implement GetContainerInstanceFromSomewhere()?
Well, I like to do it like this:
1 2 3 4 |
IWindsorContainer GetContainerInstanceFromSomewhere() { return new WindsorContainer("windsor.config"); } |
Isn’t that easy? And then I have a folder structure in my ASP.NET MVC project that looks like this:
It can be seen that I have a folder for each configuration of the system (development, test, prod) and one containing configuration files that are common for all configurations. For each configuration I have a hibernate.cfg.xml to configure NHibernate and a windsor.config which is loaded by the Windsor container in each particular configuration.
My development/windsor.config looks 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 |
<castle> <include uri="file://controllers.config"/> <include uri="file://facilities.config"/> <include uri="file://factories.config"/> <include uri="file://repositories.config"/> <include uri="file://services.config"/> <!-- For this configuration only --> <components> <component id="services.ApplicationSettings" service="Model.Services.IApplicationSettings, Model.Services" type="WebSite.Stuff.DevelopmentMachineApplicationSettings, WebSite"> <parameters> <UrlBase>http://localhost:1766</UrlBase> </parameters> </component> <component id="services.EmailSender" service="Model.Services.IEmailSender, Model.Services" type="WebSite.Stuff.FakeLoggingEmailSender, WebSite" /> </components> </castle> |
It can be seen that my development configuration is used to Cassini running on port 1766 🙂 moreover, it can be seen that my development configuration is using a fake, logging email sender, which – much as you would expect – only logs the content from emails that would otherwise have been sent.
Of course, the configuration files will not automatically be available to the web application the way they are organized now – therefore, my web project has a post-build task with the following two lines:
1 2 |
xcopy "$(ProjectDir)Config\common\*.*" "$(TargetDir).." /y xcopy "$(ProjectDir)Config\development\*.*" "$(TargetDir).." /y |
– thus copying the common configuration files along with the development configuration files to the base directory of the web application.
This also means that my build script must overwrite the development configuration files when building the system in a deployment configuration. It can be achieved as simple as this (using MSBuild):
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 |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <ApplicationFiles Include="ProjectName\WebSite\Default.aspx" /> <ApplicationFiles Include="ProjectName\WebSite\Global.asax" /> <ApplicationFiles Include="ProjectName\WebSite\Global.asax.cs" /> <ApplicationFiles Include="ProjectName\WebSite\web.config" /> <ApplicationFiles Include="ProjectName\WebSite\bin\*.*" /> </ItemGroup> <PropertyGroup> <DestinationFolder>C:\Release\ProjectName</DestinationFolder> <ConfigurationFolder>ProjectName\WebSite\Config</ConfigurationFolder> </PropertyGroup> <Target Name="build"> <MSBuild Projects="ProjectName\WebSite.sln" Targets="build" StopOnFirstFailure="true" Properties="Configuration=Release;"> <Output TaskParameter="TargetOutputs" ItemName="AssembliesBuiltByChildProjects" /> </MSBuild> </Target> <Target Name="deploy"> <RemoveDir Directories="$(DestinationFolder)\Views;$(DestinationFolder)\Content;$(DestinationFolder)\bin;$(DestinationFolder)" /> <MakeDir Directories="$(DestinationFolder);$(DestinationFolder)\bin;$(DestinationFolder)\Views;$(DestinationFolder)\Content" /> <Copy SourceFiles="@(ApplicationFiles)" DestinationFolder="$(DestinationFolder)"/> <Exec Command="xcopy /d/e/f/y ProjectName\WebSite\Views\*.* $(DestinationFolder)\Views" /> <Exec Command="xcopy /d/e/f/y ProjectName\WebSite\Content\*.* $(DestinationFolder)\Content" /> </Target> <Target Name="deploy_common_configuration_files"> <Exec Command="xcopy /d/e/f/y $(ConfigurationFolder)\common\*.* $(DestinationFolder)" /> </Target> <!-- Deployment configurations --> <Target Name="deploy_test" DependsOnTargets="build;deploy;deploy_common_configuration_files"> <Exec Command="xcopy /d/e/f/y $(ConfigurationFolder)\test\*.* $(DestinationFolder)" /> </Target> <Target Name="deploy_prod" DependsOnTargets="build;deploy;deploy_common_configuration_files"> <Exec Command="xcopy /d/e/f/y $(ConfigurationFolder)\prod\*.* $(DestinationFolder)" /> </Target> </Project> |
Here I have define tasks for compiling the web site (“build”), deploying the binaries + views + content files (“deploy”), deploying common configuration files (“deploy_common_configuration_files”), and then one task for each deployable configuration: “deploy_test” and “deploy_prod”. This makes deploying the web site on my test web server as easy as running the following command:
1 |
msbuild /t:deploy_test |
What is left now, is to make sure that the different sets of configuration files are valid in the sense that Windsor can resolve everything right. That is easily accomplished in the following NUnit test:
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 |
[TestFixture] public class TestWindsorCanResolve { const string ConfigFileRoot = @"..\..\..\WebSite\Config"; [Test] public void CanMakeControllerInstances_test() { CheckConfiguration("test"); } [Test] public void CanMakeControllerInstances_prod() { CheckConfiguration("prod"); } static void CheckConfiguration(string configuration) { var controllerTypes = GetControllerTypes(); var container = GetContainer(configuration); // using the given controllers and the given Windsor container - make sure // every controller can be instantiated controllerTypes.ForEach(t => container.Resolve(t.Name.Substring(0, t.Name.LastIndexOf("Controller")).ToLower())); } static IWindsorContainer GetContainer(string configuration) { // get path to configuration to test (inside the web site project) var path = ConfigFileRoot + Path.DirectorySeparatorChar + configuration + Path.DirectorySeparatorChar + "windsor.config"; // configure Windsor var windsorContainer = new WindsorContainer(new XmlInterpreter(path)); return windsorContainer; } static List<Type> GetControllerTypes() { // get assembly with controllers var assembly = Assembly.GetAssembly(typeof (HomeController)); var types = assembly.GetTypes(); // get all controller types return new List<Type>(types) .FindAll(t => typeof (IController).IsAssignableFrom(t) && !t.IsAbstract); } } |
This way, if I introduce a service in my development configuration that should have been implemented by a production version of the service in my production configuration, I will immediately know about it.
I think this post covers an easy and pragmatic way to control multiple configurations with Windsor and ASP.NET MVC. Of course you might want to split out the configuration-specific parts into multiple files instead of having only a windsor.config for each configuration. What I mean is something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<castle> <include uri="file://controllers-common.config"/> <include uri="file://controllers.config"/> <include uri="file://facilities-common.config"/> <include uri="file://facilities.config"/> <include uri="file://factories-common.config"/> <include uri="file://factories.config"/> <include uri="file://repositories-common.config"/> <include uri="file://repositories.config"/> <include uri="file://services-common.config"/> <include uri="file://services.config"/> </castle> |
The great thing is that we can refactor our configuration with confidence because of our test. And if we want to be extra-certain that everything works as expect, we might begin to add assertions like this:
1 2 3 4 5 6 7 8 9 10 |
public class TestWindsorCanResolve { // (...) [Test] public void OnlyProductionEnvironmentCanSpamOurSensitiveClients() { Assert.IsTrue(GetContainer("development").Resolve<IEmailSender>() is FakeLoggingEmailSender); Assert.IsTrue(GetContainer("test").Resolve<IEmailSender>() is FakeLoggingEmailSender); } |
One thought on “Working with Windsor and ASP.NET MVC”