Using Structuremap in legacy ASP.NET MVC applications
Dependency Injection (DI) was also supported by classic ASP.NET MVC but there was no framework-level dependency injection like in ASP.NET Core. In this blog post I will show how to use Structuremap for dependency injection in ASP.NET MVC applications and how to resolve dependencies using built-in components in classes that doesn’t support dependency injection.
Legacy warning! The code here and solutions are for classic ASP.NET MVC. If you are working on legacy application written on it and you need to introduce dependency injection there or you need to get rid of ObjectFactory class of old Structuremap then this post is for you. In this post I stay in context of legacy app I’m working on and I don’t want to introduce any big changes to code base.
As long as we work in Application class or controllers things are quiet easy. At application class we create DI container, configure it and then set up controller factory that we introduce to ASP.NET MVC.
But there are ASP.NET and ASP.NET MVC components that doesn’t support dependency injection. Some examples:
- layout pages and views
- membership and roles providers
- controller attributes
There’s no straightforward way to make those things use dependency injection like we want. There was ObjectFactory class in Structuremap but it is deprecated. As a last option we can use Service Locator pattern (once pattern but now anti-pattern). It provides us with static instance that resolves dependencies for us.
But let’s get to dependency injection step by step.
Using Structuremap for dependency injection
I am long time user of Structuremap. It is DI/IoC container that supports assembly scanning and dependency definition registries. Registries are simple classes where containers are configured.
Here’s the primitive example of Structuremap registry.
public class MyRegistry : Registry
{
public MyRegistry()
{
For<IDataMapper>().Use<NHibernateMapperSession>()
.LifecycleIs<ThreadLocalStorageLifecycle>();
For<ILogger>().Use<NLogWrapper>()
.LifecycleIs<ThreadLocalStorageLifecycle>();
For<IInvoiceGenerator>().Use<InvoiceGenerator>()
.LifecycleIs<ThreadLocalStorageLifecycle>();
}
}
And here’s how Structuremap containers are configured (scanning is also possible).
var container = new Container(x =>
{
x.AddRegistry<MyRegistry>();
x.AddRegistry<SomeOtherRegistry>();
});
If instance is needed then it’s easy to get it from container.
var logger = container.GetInstance<ILogger>();
It’s all nice but it’s not enough to get dependency injection to classic ASP.NET MVC. We need some more pieces.
Building controller factory
Controller factory is MVC component that builds controller instances when framework needs one. We need custom controller factory for Structuremap to get dependencies resolved and controller instance built when request comes in.
public class StructuremapControllerFactory : DefaultControllerFactory
{
private readonly Container _container;
public StructuremapControllerFactory(Container container)
{
_container = container;
}
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (controllerType == null)
{
return null;
}
return _container.GetInstance(controllerType) as Controller;
}
}
We have to introduce out controller factory to MVC when web application starts.
var factory = new StructuremapControllerFactory(container);
ControllerBuilder.Current.SetControllerFactory(factory);
Notice that previously initialized Structuremap container is provided to controller factory.
Now we can write controllers like we are doing it right now in ASP.NET Core MVC using framework-level dependency injection.
public class MyDemoController : Controller
{
private readonly ILogger _logger;
private readonly IAlertService _alertService;
public MyDemoController(ILogger logger, IAlertService alertService)
{
_logger = logger;
_alertService = alertService;
}
// Controller actions follow
}
But this not the end of the show – we have some famous components that doesn’t go through dependency injection.
Using built-in service locator
Now let’s focus on our old friends like role provider, membership providers and others that don’t support dependency injection. For these components we have to go with built-in service locator.
[See võiks ehk sinine olla?] Service locator is static class we can use everywhere in our code to resolve dependencies. I don’t like this pattern much because resolving of dependencies happens inside class that needs these and therefore class has one more responsibility.
ASP.NET MVC provides us with DependencyResolver class. By it’s nature it is typical service locator. Here’s how we can use it to return list of roles in classic ASP.NET roles provider.
public class MyRoleProvider : RoleProvider
{
// ...
public override string[] GetAllRoles()
{
var repo = DependencyResolver.Current.GetService<IRoleRepository>();
var roles = from r in repo.ListAll()
select r.Name;
return roles.ToArray();
}
// ...
}
No constructor injection, no property injection – just a primitive call to static scope.
Getting DependencyResolver work with Structuremap is a little bit tricky. We have to write Structuremap based dependency resolver that DependencyResolver can internally use when detecting instances. We don’t have to invent the wheel as there’s excellent blog post Configuring MVC 4 with StructureMap by Bia Securities blog. We need StructureMapDependencyResolver and StructureMapDependencyScope classes.
public class StructureMapDependencyResolver : StructureMapDependencyScope, IDependencyResolver
{
public StructureMapDependencyResolver(IContainer container) : base(container)
{
}
public IDependencyScope BeginScope()
{
var child = Container.GetNestedContainer();
return new StructureMapDependencyResolver(child);
}
}
public class StructureMapDependencyScope : ServiceLocatorImplBase, IDependencyScope
{
protected readonly IContainer Container;
public StructureMapDependencyScope(IContainer container)
{
if (container == null)
{
throw new ArgumentNullException(nameof(container));
}
Container = container;
}
public override object GetService(Type serviceType)
{
if (serviceType == null)
{
return null;
}
return serviceType.IsAbstract || serviceType.IsInterface
? Container.TryGetInstance(serviceType)
: Container.GetInstance(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType)
{
return Container.GetAllInstances(serviceType).Cast<object>();
}
protected override IEnumerable<object> DoGetAllInstances(Type serviceType)
{
return Container.GetAllInstances(serviceType).Cast<object>();
}
protected override object DoGetInstance(Type serviceType, string key)
{
if (string.IsNullOrEmpty(key))
{
return serviceType.IsAbstract || serviceType.IsInterface
? Container.TryGetInstance(serviceType)
: Container.GetInstance(serviceType);
}
return Container.GetInstance(serviceType, key);
}
private bool disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if(Container != null)
{
Container.Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
Finally we have to introduce our custom dependency resolver to ASP.NET MVC. We will do it in Application_Start method of Application class.
var mvcDependencyResolver = new StructureMapDependencyResolver(_container);
DependencyResolver.SetResolver(mvcDependencyResolver);
var factory = new StructuremapControllerFactory(_container);
ControllerBuilder.Current.SetControllerFactory(factory);
Now we have dependencies available also in components that doesn’t support dependency injection.
Conclusion
Although I don’t like service locator much it’s sometimes the only option in classic ASP.NET MVC to resolve dependencies in components that doesn’t support dependency injection. Good thing is that we can still use DI/IoC container we like and some containers come with special libraries or extension methods that attach them automatically to ASP.NET MVC components. Making sure that dependency injection is done properly is mandatory step before moving system over to ASP.NET Core.
Pingback:Dew Drop – August 18, 2020 (#3256) | Morning Dew