However for the uninitiated (and even for those how have worked with it for some time) you can run into unexpected errors, especially when mixing Lifestyles for different Components on larger projects where you have multiple container installers/initializers.
I will here present a very obvious case that nevertheless had me and another developer spending a few hours trying to figure out what was wrong. This bug presented here are especially difficult to track down when you are doing frequent builds on a development machine and that build process causes the IIS application pool(s) to recycle, effectively resetting the IoC container.
I will be using Castle Windsor as the IoC (Inversion of Control) container in this posting.
The zipped solution can be downloaded here IocLifestyles.zip
First of all we will need to start with a new standard empty ASP.MVC NET 3.0 site/project and add a reference to Castle.Windsor. This reference can be added using NuGet or by downloading the source or binary libraries at the Castle Windsor website.
Secondly we need to tell ASP.NET MVC that we do not want to use the DefaultControllerFactory to create controller instances but that we want to use our own which in turn uses Windsor to provide the dependencies the various controllers need.
I do this in the Global.asax file by adding the following lines in the Application_Start method.
// Using WindsorControllerFactory rather then DependencyResolver // http://bradwilson.typepad.com/blog/2010/07/service-location-pt1-introduction.html (read comments) // http://bradwilson.typepad.com/blog/2010/10/service-location-pt5-idependencyresolver.html (read comments) // http://bradwilson.typepad.com/blog/2010/10/service-location-pt10-controller-activator.html (read comments) // http://mikehadlow.blogspot.com/2011/02/mvc-30-idependencyresolver-interface-is.html // http://kozmic.pl/2010/08/19/must-windsor-track-my-components/ ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(applicationWideWindsorContainer)); // Initialize / install components in container applicationWideWindsorContainer.Install(new WindsorInstaller());As well as the the field
WindsorContainer applicationWideWindsorContainer = new WindsorContainer();This leads to our Global.asax files looking like this
using System.Web.Mvc; using System.Web.Routing; using Castle.Windsor; namespace IocLifestyles { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); } public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Default", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); } WindsorContainer applicationWideWindsorContainer = new WindsorContainer(); protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); // Using WindsorControllerFactory rather then DependencyResolver // http://bradwilson.typepad.com/blog/2010/07/service-location-pt1-introduction.html (read comments) // http://bradwilson.typepad.com/blog/2010/10/service-location-pt5-idependencyresolver.html (read comments) // http://bradwilson.typepad.com/blog/2010/10/service-location-pt10-controller-activator.html (read comments) // http://mikehadlow.blogspot.com/2011/02/mvc-30-idependencyresolver-interface-is.html // http://kozmic.pl/2010/08/19/must-windsor-track-my-components/ ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(applicationWideWindsorContainer)); // Initialize / install components in container applicationWideWindsorContainer.Install(new WindsorInstaller()); } } }
We now need to look closer at 2 classes, namely the WindsorControllerFactory and the WindsorInstaller.
The responsibility of the WindsorControllerFactory is to provide all the objects to the controller which it depends on to perform it's logic. This is passed in to the controllers constructor. This is done using Constructor Injection (you can see a lot of good tips, thoughts and recommendations at Mark Seeman's blog).
using System; using System.Web.Mvc; using System.Web.Routing; using Castle.Windsor; namespace IocLifestyles { public class WindsorControllerFactory : DefaultControllerFactory { private readonly IWindsorContainer windsorContainer; public WindsorControllerFactory(IWindsorContainer windsorContainer) { this.windsorContainer = windsorContainer; } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return windsorContainer.Resolve(controllerType) as IController; } public override void ReleaseController(IController controller) { var disposableController = controller as IDisposable; if (disposableController != null) { disposableController.Dispose(); } windsorContainer.Release(controller); } } }Now we need to register the classes which we are going to use through out our application with the IoC container and we do this by implementing the IWindsorInstaller interface.
Please note the Lifestyles for the different components, since this is the key to the bug I am going to illustrate.
using System.Web.Mvc; using Castle.Facilities.FactorySupport; using Castle.MicroKernel.Registration; using Castle.MicroKernel.SubSystems.Configuration; using Castle.Windsor; using System.Web; namespace IocLifestyles { public class WindsorInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { // Register all controllers from this assembly container.Register( AllTypes.FromThisAssembly() .BasedOn<Controller>() .Configure(c => c.LifeStyle.PerWebRequest) ); // Register HttpContext(Base) and HttpRequest(Base) so it automagically can be injected using IoC container.AddFacility<FactorySupportFacility>(); container.Register(Component.For<HttpRequestBase>().LifeStyle.PerWebRequest .UsingFactoryMethod(() => new HttpRequestWrapper(HttpContext.Current.Request))); container.Register(Component.For<HttpContextBase>().LifeStyle.PerWebRequest .UsingFactoryMethod(() => new HttpContextWrapper(HttpContext.Current))); // Respository and Service registrations container.Register(Component.For<ISomeRepository>().ImplementedBy<SomeRepository>()); container.Register(Component.For<ISessionValidator>().ImplementedBy<SessionValidator>().LifeStyle.PerWebRequest); } } }
Looking closer at the 2 classes that we have registered in the WindsorInstaller: SomeRepository and SessionValidator.
namespace IocLifestyles { public interface ISomeRepository { SessionToken GetSessionToken(); } public class SomeRepository : ISomeRepository { private readonly ISessionValidator sessionValidator; public SomeRepository(ISessionValidator sessionValidator) { this.sessionValidator = sessionValidator; } public SessionToken GetSessionToken() { return sessionValidator.ValidateSession(); } } }
using System.Net; using System.Web; namespace IocLifestyles { public interface ISessionValidator { SessionToken ValidateSession(); } public class SessionValidator : ISessionValidator { private readonly HttpContextBase httpContextBase; public SessionValidator(HttpContextBase httpContextBase) { this.httpContextBase = httpContextBase; } public SessionToken ValidateSession() { // Do some validation here var sessionToken = new SessionToken { IpAddress = IPAddress.Parse(httpContextBase.Request.UserHostAddress), IsValid = true }; return sessionToken; } } }The final 2 pieces needed to actually see something in the browser is the DefaultController
using System.Web.Mvc; namespace IocLifestyles.Controllers { public class DefaultController : Controller { private readonly ISomeRepository someRepository; // Constructor Injection public DefaultController(ISomeRepository someRepository) { this.someRepository = someRepository; } public ActionResult Index() { ViewData.Model = someRepository.GetSessionToken(); return View(); } } }
and the Views/Default/Index.cshtml view.
@model IocLifestyles.SessionToken @{ ViewBag.Title = "Index"; } <h2> SessionToken IpAddress: @Model.IpAddress.ToString()</h2>
This is a rather contrived example, but please bear with me for the sake of demonstration purposes.
What is interesting however is what happens when we run the application the first time and contrast this to what happens when we run it a second time without rebuilding the code.
First time:
Second and subsequent times:
Playing around with this you will notice that you can display the page one time after a build, this is giving us an indication of what is wrong.
Below you can see the full stack trace with all details
Server Error in '/' Application. Object reference not set to an instance of an object. Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code. Exception Details: System.NullReferenceException: Object reference not set to an instance of an object. Source Error: Line 20: { Line 21: // Do some validation here Line 22: var sessionToken = new SessionToken Line 23: { Line 24: IpAddress = IPAddress.Parse(httpContextBase.Request.UserHostAddress), Source File: D:\Users\kst\Visual Studio 2010\projects\IocLifestyles\IocLifestyles\SessionValidator.cs Line: 22 Stack Trace: [NullReferenceException: Object reference not set to an instance of an object.] Microsoft.VisualStudio.WebHost.Connection.get_RemoteIP() +0 Microsoft.VisualStudio.WebHost.Request.GetRemoteAddress() +65 System.Web.HttpRequestWrapper.get_UserHostAddress() +22 IocLifestyles.SessionValidator.ValidateSession() in D:\Users\kst\Visual Studio 2010\projects\IocLifestyles\IocLifestyles\SessionValidator.cs:22 IocLifestyles.SomeRepository.GetSessionToken() in D:\Users\kst\Visual Studio 2010\projects\IocLifestyles\IocLifestyles\SomeRepository.cs:19 IocLifestyles.Controllers.DefaultController.Index() in D:\Users\kst\Visual Studio 2010\projects\IocLifestyles\IocLifestyles\Controllers\DefaultController.cs:17 lambda_method(Closure , ControllerBase , Object[] ) +96 System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) +17 System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters) +208 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +27 System.Web.Mvc.<>c__DisplayClass15.<InvokeActionMethodWithFilters>b__12() +55 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation) +263 System.Web.Mvc.<>c__DisplayClass17.<InvokeActionMethodWithFilters>b__14() +19 System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodWithFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor, IDictionary`2 parameters) +191 System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName) +343 System.Web.Mvc.Controller.ExecuteCore() +116 System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext) +97 System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext) +10 System.Web.Mvc.<>c__DisplayClassb.<BeginProcessRequest>b__5() +37 System.Web.Mvc.Async.<>c__DisplayClass1.<MakeVoidDelegate>b__0() +21 System.Web.Mvc.Async.<>c__DisplayClass8`1.<BeginSynchronous>b__7(IAsyncResult _) +12 System.Web.Mvc.Async.WrappedAsyncResult`1.End() +62 System.Web.Mvc.<>c__DisplayClasse.<EndProcessRequest>b__d() +50 System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(Action f) +7 System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Action action) +22 System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +60 System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +9 System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +8862285 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +184 Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.0.30319.208
The reason this is failing is the mixing of Lifestyles in the WindsorInstaller. Lets bring up that particular code again.
using System.Web.Mvc; using Castle.Facilities.FactorySupport; using Castle.MicroKernel.Registration; using Castle.MicroKernel.SubSystems.Configuration; using Castle.Windsor; using System.Web; namespace IocLifestyles { public class WindsorInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { // Register all controllers from this assembly container.Register( AllTypes.FromThisAssembly() .BasedOn<Controller>() .Configure(c => c.LifeStyle.PerWebRequest) ); // Register HttpContext(Base) and HttpRequest(Base) so it automagically can be injected using IoC container.AddFacility<FactorySupportFacility>(); container.Register(Component.For<HttpRequestBase>().LifeStyle.PerWebRequest .UsingFactoryMethod(() => new HttpRequestWrapper(HttpContext.Current.Request))); container.Register(Component.For<HttpContextBase>().LifeStyle.PerWebRequest .UsingFactoryMethod(() => new HttpContextWrapper(HttpContext.Current))); // Respository and Service registrations container.Register(Component.For<ISomeRepository>().ImplementedBy<SomeRepository>()); container.Register(Component.For<ISessionValidator>().ImplementedBy<SessionValidator>().LifeStyle.PerWebRequest); } } }
The default lifestyle is Singleton. Omitting the lifestyle on ISomeRepository will register it as a Lifestyle.Singleton. SomeRepository will be instantiated one time by Windsor and then cached internally for future use. This means that even though all the other registered Components are Lifestyle.PerWebRequest, ISomeRepository will not be able to benefit from this.
There are no warnings that helps you if you mix Lifestyles in your installers, and especially in larger projects, this can be a challenge when you start tweaking for memory/performance reasons.
You need to know how all the dependencies work, and how they are used, to be able tweak Lifestyles for components. If you do not have this knowledge the risk is very high that you will introduce bugs. My suggestion especially if you are new to ASP.NET MVC leave Lifestyles to Lifestyle.PerWebRequest for all components, unless it is very obvious that this is a real singleton component. Only start tweaking Lifestyles if performance or memory consumption becomes a problem.
Changing the installer to the following solves the problem.
using System.Web.Mvc; using Castle.Facilities.FactorySupport; using Castle.MicroKernel.Registration; using Castle.MicroKernel.SubSystems.Configuration; using Castle.Windsor; using System.Web; namespace IocLifestyles { public class WindsorInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { // Register all controllers from this assembly container.Register( AllTypes.FromThisAssembly() .BasedOn<Controller>() .Configure(c => c.LifeStyle.PerWebRequest) ); // Register HttpContext(Base) and HttpRequest(Base) so it automagically can be injected using IoC container.AddFacility<FactorySupportFacility>(); container.Register(Component.For<HttpRequestBase>().LifeStyle.PerWebRequest .UsingFactoryMethod(() => new HttpRequestWrapper(HttpContext.Current.Request))); container.Register(Component.For<HttpContextBase>().LifeStyle.PerWebRequest .UsingFactoryMethod(() => new HttpContextWrapper(HttpContext.Current))); // Respository and Service registrations container.Register(Component.For<ISomeRepository>().ImplementedBy<SomeRepository>().LifeStyle.PerWebRequest); container.Register(Component.For<ISessionValidator>().ImplementedBy<SessionValidator>().LifeStyle.PerWebRequest); } } }
5 comments:
VERY NICE!!!
Would it be possible to add a few named
regexes for postal codes, phone numbers
etc.
Also, would it be possible to add a very simple hook for calling a user-defined javascript function?
Oh wait -- I see you actually already showed how to do this in another article. EXCELLENT!!!!
http://kenneththorman.blogspot.com/2010/08/custom-javascript-validation-scripts-in.html
Just found this posting (http://www.devtrends.co.uk/blog/how-not-to-do-dependency-injection-the-static-or-singleton-container) which I think nicely explains about the dos & don'ts of DI and IoC
Good, but I dont get the SessionToken. Still error in that part.
Ah, I get it. Thanks.
Post a Comment