Reputation: 72760
I'm working on an ASP.NET MVC4 application, using Unity 3 as DI framework. I designed the architecture in a modular way, creating several satellite library projects (facade, managers, DB adapters, etc.), all statically linked to the app (which means added as references to the web project). The reason behind such architecture is to allow reusage in other contexts (for instance a separate REST based backend service, implemented as a standalone web project).
I'm using dependency injection, which occurs at web project level, but also in the satellite DLLs. In order to centralize DI resolution, I'm using a simple interface
public interface IIocInstaller
{
void Setup(IUnityContainer container);
}
and each library project has a class implementing that interface. Using reflection, the web app scans all loaded assemblies for a class implementing the interface, and calls the Setup method.
Everything works when I start the app. However, when the web app is unloaded by the web server due to inactivity, on next request the following exception is thrown:
[InvalidOperationException: The type ICustomerFacade does not have an accessible constructor.]
Microsoft.Practices.ObjectBuilder2.DynamicMethodConstructorStrategy.ThrowForNullExistingObject(IBuilderContext context) +178
lambda_method(Closure , IBuilderContext ) +25
Microsoft.Practices.ObjectBuilder2.<>c__DisplayClass1.<GetBuildMethod>b__0(IBuilderContext context) +35
Microsoft.Practices.ObjectBuilder2.DynamicMethodBuildPlan.BuildUp(IBuilderContext context) +10
Microsoft.Practices.ObjectBuilder2.BuildPlanStrategy.PreBuildUp(IBuilderContext context) +196
Microsoft.Practices.ObjectBuilder2.StrategyChain.ExecuteBuildUp(IBuilderContext context) +193
Microsoft.Practices.ObjectBuilder2.BuilderContext.NewBuildUp(NamedTypeBuildKey newBuildKey) +113
Microsoft.Practices.Unity.ObjectBuilder.NamedTypeDependencyResolverPolicy.Resolve(IBuilderContext context) +48
lambda_method(Closure , IBuilderContext ) +111
Microsoft.Practices.ObjectBuilder2.<>c__DisplayClass1.<GetBuildMethod>b__0(IBuilderContext context) +35
Microsoft.Practices.ObjectBuilder2.DynamicMethodBuildPlan.BuildUp(IBuilderContext context) +10
Microsoft.Practices.ObjectBuilder2.BuildPlanStrategy.PreBuildUp(IBuilderContext context) +196
Microsoft.Practices.ObjectBuilder2.StrategyChain.ExecuteBuildUp(IBuilderContext context) +193
Microsoft.Practices.Unity.UnityContainer.DoBuildUp(Type t, Object existing, String name, IEnumerable`1 resolverOverrides) +165
[ResolutionFailedException: Resolution of the dependency failed, type = "Eden.SMS.UI.Web.Controllers.CustomersController", name = "(none)".
Exception occurred while: while resolving.
Exception is: InvalidOperationException - The type ICustomerFacade does not have an accessible constructor.
-----------------------------------------------
At the time of the exception, the container was:
Resolving Eden.SMS.UI.Web.Controllers.CustomersController,(none)
Resolving parameter "customerFacade" of constructor Eden.SMS.UI.Web.Controllers.CustomersController(Eden.SMS.Service.Facades.Interface.ICustomerFacade customerFacade)
Resolving Eden.SMS.Service.Facades.Interface.ICustomerFacade,(none)
]
Microsoft.Practices.Unity.UnityContainer.DoBuildUp(Type t, Object existing, String name, IEnumerable`1 resolverOverrides) +329
Microsoft.Practices.Unity.UnityContainer.Resolve(Type t, String name, ResolverOverride[] resolverOverrides) +15
Microsoft.Practices.Unity.UnityContainerExtensions.Resolve(IUnityContainer container, Type t, ResolverOverride[] overrides) +18
Unity.Mvc4.UnityDependencyResolver.GetService(Type serviceType) +67
System.Web.Mvc.DefaultControllerActivator.Create(RequestContext requestContext, Type controllerType) +41
[InvalidOperationException: An error occurred when trying to create a controller of type 'Eden.SMS.UI.Web.Controllers.CustomersController'. Make sure that the controller has a parameterless public constructor.]
System.Web.Mvc.DefaultControllerActivator.Create(RequestContext requestContext, Type controllerType) +178
System.Web.Mvc.DefaultControllerFactory.GetControllerInstance(RequestContext requestContext, Type controllerType) +77
System.Web.Mvc.DefaultControllerFactory.CreateController(RequestContext requestContext, String controllerName) +66
System.Web.Mvc.MvcHandler.ProcessRequestInit(HttpContextBase httpContext, IController& controller, IControllerFactory& factory) +191
System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state) +50
System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state) +48
System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData) +16
System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +301
System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155
All unity related code is performed in Application_Start. The ICustomerFacade interface is injected in a controller, and it is subject itself to injection of other instances, of course still using DI. The error message warns about the controller having a parameterless constructor, which is of course not implemented because I need the one with injected parameters - I've also tried to specify the constructor to be used (by using InjectionConstructor), but with no luck.
Any idea why that exception is thrown?
Update
It looks like the problem is due to failure invoking the initializer in the satellite projects.
This is the code that I'm using to scan all assemblies looking for instances implementing the IIocInstaller interface
public class AssemblyInjector
{
/// <summary>
/// Initialize IoC in all assemblies implementing the IocExportAssembly attribute
/// </summary>
/// <param name="container"></param>
/// <param name="assemblyPrefix">The prefix of the assembly names to load</param>
public static void RegisterAssemblyTypes(IUnityContainer container, string assemblyPrefix)
{
var bootstrapers = EnumerateIocBootstraperTypes(assemblyPrefix);
foreach ( Type type in bootstrapers )
{
var instance = (IIocInstaller) Activator.CreateInstance(type);
instance.Setup(container);
}
}
/// <summary>
/// Given a list of assemblies, find all types exposing the
/// <see cref="IocExportAssemblyAttribute"/> attribute
/// </summary>
/// <param name="assemblyPrefix">The prefix of the assembly names to load</param>
/// <returns>list of types exposing the <see cref="IocExportAssemblyAttribute"/> attribute</returns>
private static IEnumerable<Type> EnumerateIocBootstraperTypes(string assemblyPrefix)
{
var assemblies = EnumerateIocAssemblies(assemblyPrefix);
var iocInterface = typeof(IIocInstaller);
var bootstrapers = assemblies.SelectMany(s => s.GetTypes())
.Where(iocInterface.IsAssignableFrom);
return bootstrapers;
}
/// <summary>
/// Enumerate and return all assemblies whose name starts by "Eden.SMS"
/// </summary>
/// <returns><see cref="IEnumerable{T}"/>list of assemblies</returns>
/// <param name="assemblyPrefix">The prefix of the assembly names to load</param>
private static IEnumerable<Assembly> EnumerateIocAssemblies(string assemblyPrefix)
{
return from assembly in AppDomain.CurrentDomain.GetAssemblies()
where assembly.GetName().Name.StartsWith(assemblyPrefix)
select assembly;
}
}
and this is used in the Unity bootstrapper
/// <summary>
/// Dependency injection bootstrapper
/// </summary>
public static class Bootstrapper
{
private const string ASSEMBLY_PREFIX = "Eden.SMS";
public static IUnityContainer Initialise()
{
var container = BuildUnityContainer();
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
return container;
}
private static IUnityContainer BuildUnityContainer()
{
var container = new UnityContainer();
AssemblyInjector.RegisterAssemblyTypes(container, ASSEMBLY_PREFIX);
//RegisterSatelliteTypes(container);
// Register local types
RegisterTypes(container);
foreach ( var registration in container.Registrations )
{
Debug.WriteLine(string.Format("Registered {0} as {1}", registration.RegisteredType, registration.MappedToType));
}
return container;
}
private static void RegisterSatelliteTypes(UnityContainer container)
{
new Eden.SMS.Data.Adapters.Mssql.Bootstrapper().Setup(container);
new Eden.SMS.Data.Managers.Bootstrapper().Setup(container);
new Eden.SMS.Service.Bootstrapper().Setup(container);
}
private static void RegisterTypes(IUnityContainer container)
{
container.RegisterType<IController, CustomersController>(new InjectionConstructor(typeof(ICustomerFacade)));
container.RegisterType<IController, AuthenticationController>(new InjectionConstructor(typeof(IAuthenticationFacade)));
}
}
The bootstrapper, in turns, is called from Application_Start() in Global.asax.
If in bootstrapper I comment this line
AssemblyInjector.RegisterAssemblyTypes(container, ASSEMBLY_PREFIX);
and uncomment this
RegisterSatelliteTypes(container);
it works as expected (no exception thrown after the app is unloaded by the web server). That change disables the dynamic lookup of classes implementing the IIocInstaller, replacing that by direct calls on each satellite assembly.
This workaround fixes the bug, but I'd want to stick with the original "dynamic" plan. So I'd like to figure out why that is happening and how to solve it. Any suggestion?
Upvotes: 2
Views: 645
Reputation: 34992
I believe your issue is what this other post describes about the way DLLs are loaded into the AppDomain by the .Net Framework:
The .NET Framework defers loading assemblies into the current AppDomain until they're needed. For example, if you call into a third-party library only from SomeMethod(), the third-party DLL normally won't be loaded until the first time SomeMethod() runs.
AppDomain.GetAssemblies() gives you all assemblies which have already been loaded into the current AppDomain.
The good news are that there is method BuildManager.GetReferencedAssemblies()
that returns a list of all assemblies referenced from Web.config and elsewhere, and it loads those assemblies into the current AppDomain.
This was the cause for this issue similar to yours, which was solved using BuildManager.GetReferencedAssemblies()
instead of AppDomain.CurrentDomain.GetAssemblies()
.
Hope it helps!
Upvotes: 3