I've been writing a little bit about Autofac and why it's rad, but today I want to talk about Autofac modules. In my previous post on this, I talked about one of the drawbacks to the constructor dependency pattern is that at some point in your application, generally in the entry point, you get allllll of this spaghetti code that is the setup for your code base.
Essentially, we've balanced having nice clean testable classes with having a really messy spot in the code. But it's only ONE spot and the rest of your code is nice. So it's a decent trade-off. But we can do better than that, can't we?
What are Autofac Modules?
We can use Autofac modules to organize some of the code that we have in our entry point into logical groupings. So an Autofac module is an implementation of a class that registers types to our dependency container to be resolved at a later time. You could do this all in one big module, but like many things in programming, having some giant monolithic thing that does ALLLL the work usually isn't the best.
An Example of Converting to Autofac Modules
Let's create a simple application as an example. I'll describe it in words, and then I'll toss up some code to show a simple representation of it. We'll assume we're using dependencies passed as interfaces via constructors as one of our best practices, which makes this conversion much easier!
So our app will have a main window with a main content area and a header area. These will be represented by three objects. Our application will also have a logger instance that we pass around so classes that need logging abilities can take an ILogger in their constructor. But our logger will have some simple configuration that we need to do before we use it.
Let's assume to start our Program.cs file looks like this:
internal sealed class Program
{
private static void Main(string[] args)
{
var logger = new FileLogger();
logger.LogLevel = LogLevel.Debug;
logger.FilePath = "log.txt";
var header = new FancyHeader(logger);
var content = BoringMainContent();
var window = new MainWindow(header, content);
window.Show();
}
}
Before getting comfortable with Autofac, my initial first step would be to logically group things in the main method. In this particular case, we have something simple and surprise... It's all grouped. But my next step would usually be to pull these things out into their own methods. I do this because it helps me identify if my groupings make sense and where my dependencies are. Let's try it!
internal sealed class Program
{
private static void Main(string[] args)
{
var logger = InitializeLogging();
var window = InitializeGui(logger);
window.Show();
}
// no params passed in, so no dependencies
// return value is an ILogger, so we have a
// logical grouping that will provide us a logger
private static ILogger InitializeLogging()
{
var logger = new FileLogger();
logger.LogLevel = LogLevel.Debug;
logger.FilePath = "log.txt";
return logger;
}
// only parameter is a logger, so that's our dependency
// return value is a window, so this grouping provides
// a window for us
private IWindow InitializeGui(ILogger logger)
{
var header = new FancyHeader(logger);
var content = BoringMainContent();
var window = new MainWindow(header, content);
return window;
}
}
Alright cool. So yes, this is a bit of extra code compared to the initial example, but I promise you that grouping these things out into separate methods as a starting point when you have a LOT of initialization logic will help a ton. Once they are in methods, you can pull them out into their own classes. Refactoring 101 for the single responsibility principle going on here ;) BUT, we're interested in Autofac. So what's the next step?
We have two logical groupings going on here in our example. One is logging and the other is for the GUI. So we can actually go ahead and make two Autofac modules that do this work for us.
public sealed class LoggingModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.RegisterType<FileLogger>()
.AsImplementedInterfaces() // FileLogger will be resolved as an ILogger
.SingleInstance() // we only ever need to use one logger instance for our app
.OnActivated(x =>
{
// this handles our extra setup we had for this object
x.Instance.LogLevel = LogLevel.Debug;
x.Instance.FilePath = "log.txt";
});
}
}
public sealed class GuiModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.RegisterType<FancyHeader>() // this has a dependency on ILogger, but autofac will figure it out for us
.AsImplementedInterfaces() // FancyHeader will be resolved as IHeader
.SingleInstance(); // we only ever need to use one instance for our app
builder
.RegisterType<BoringMainContent>()
.AsImplementedInterfaces() // BoringMainContent will be resolved as IContent
.SingleInstance(); // we only ever need to use one instance for our app
builder
.RegisterType<MainWindow>() // Autofac will resolve our IHeader and IContent dependencies for us
.AsImplementedInterfaces() // MainWindow will be resolved as IWindow
.SingleInstance(); // we only ever need to use one instance for our app
}
}
And those are our two logical groupings for modules! So, how do we use this, and what does our Main() method look like now? I'll demonstrate one way that works for a couple of modules, but I want to follow up with another post that talks about dynamically loading modules. If you can imagine this scenario blown out across MANY modules, you'll understand why it might be helpful.
The idea for our Main() method is that we just want to resolve the one main dependency manually and let Autofac do the rest. So in this case, it's our MainWindow.
private static void Main(string[] args)
{
// create an autofac container builder
var containerBuilder = new ContainerBuilder();
// manually register our two new modules we made
containerBuilder.RegisterModule<LoggingModule>();
containerBuilder.RegisterModule<GuiModule >();
// create the dependency container
var container = containerBuilder.Build();
// resolve and use our main dependency by it's interface
// (because we shouldn't care what the implementation is...
// that was up to the configuration via modules!)
var window = container.Resolve<IWindow>();
window.Show();
}
In Summary...
This example showed us how to group your main initialization logic into groups that would play nice as Autofac modules. In a really simple example, having modules might look like bloated extra code, but it already illustrated that your entry point is very simple and follows a pattern to extend (just register another module for more dependencies... and I'll add more on this later). There's also an obvious way to group more new logic into your application for dependencies! So discussed logging and GUI initialization, but you could extend this to:
- User Settings
- Analytics/Telemetry
- Error Reporting
- Database Configuration
- Etc... Just add more modules!
Sometimes the pain of having a really hectic entry point isn't realized until you've had to work on teams where people are modifying the same beast of an entry point all the time:
- Simple merge conflicts in your "using" statements... Because there are hundreds of lines of using statements at the top of the file
- Visual Studio actually CANNOT use intellisense properly when the file gets too unwieldy
- The debugger cannot resolve variables properly when the main entry point gets too big
- Merging and auto-conflict resolution sometimes results in code just getting blown away in the entry point... And good luck finding what went wrong in your thousands of lines of initialization
So what's next? Well, if you keep building out your app you might notice you have tons of modules now. Your single GUI module might have to get broken out into modules for certain parts of the GUI, for example, just to keep them more manageable. Maybe you want plugins to extend the application dynamically, which is really powerful! Our method for registering modules just isn't really extensible at that point, but it's very explicit. I'll be sharing some information about automatic Autofac module discovery and registration next!