One of the things discussed was the separation of concerns, where Steve discusses creating an architecture in which you try to break up your application in such a way that hides implementation detail in one project from another consuming project. Instead, the consuming project is only aware of interfaces or abstract classes from shared libraries from which instances are are created by the dependency injection framework in use.
The aim is to try and guide a developer of the consuming project away from ‘new-ing’ up instances of a class from outside the project. To use Steve's catch phrase, "new is glue".
I was listening to the podcast on my commute to work and it got me thinking about the project I had just started working on. So much so, that I had to put the podcast on pause to give myself some thinking time for the second half of the commute.
What was causing the sparks to go off in my head was about how dependencies are registered in the Startup class in ASP.Net Core.
By default, when you create a new ASP.Net Core project, the Startup class is created as part of that project, and it is here that you register your dependencies. If your dependencies are in another project/assembly/Nuget package, it means that the references to wherever the dependency is has to be added to the consuming project.
Of course, if you do this, that means that the developer of the consuming project is free to ‘new up’ an instance of a dependency rather than rely on the DI container. The gist of Steve Smith’s comment in the podcast was do what you can to help try to prevent this.
When I got to work, I had a look at the code and pondered about whether the Startup class could be moved out to another project. That way the main ASP.Net project would only have a reference to the new project (we’ll call it the infrastructure project for simplicity) and not the myriad of other projects/Nugets. Simple huh? Yeah right!
So the first problem I hit was all the ASP/MVC plumbing that would be needed in the new project. When I copied the Startup class to the new project, Visual Studio started moaning about all the missing references.
Now when you create a new MVC/Web.API project with .Net Core, the VS template uses the Microsoft.AspNetCore.All meta NuGet package. For those not familiar with meta packages, these are NuGet packages that bundle up a number of other NuGet packages – and Microsoft.AspNetCore.All is massive. When I opened the nuspec file from the cache on my machine, there were 136 dependencies on other packages. For my infrastructure project, I was not going to need all of these. I was only interested in the ones required to support the interfaces, classes and extension methods I would need in the Startup class.
Oh boy, that was a big mistake. It was a case of adding all the dependencies I would actually need one by one to ensure I was not bringing any unnecessary packages along for the ride. Painful, but I did it.
So I made all the updates to the main MVC project required to use the Startup class from my new project and remove the references I previously had to other projects (domain, repository etc) as this was the point of the exercise.
It all compiled! Great. Pressed F5 to run and … hang on what?
After a bit of head scratching, I realised the problem was that MVC could not find the controller? WHY?
At this point, I parked my so-called ‘best practice’ changes as I did not want to waste valuable project time on a wild goose chase.
This was really bugging me, so outside of work, I started to do some more digging.
After reading some blogs and looking at the source code in GitHub, the penny dropped. ASP.Net MVC makes the assumption that the controllers are in the same assembly as the Startup class.
When the Startup class is registered with the host builder, it sets the ApplicationName property in HostingEnvironment instance to the name of the assembly where the Startup class is.
The ApplicationName property of the IHostingEnvironment instance is used by the AddMvc extension to register the assembly where controllers can be found.
Eventually, I found the workaround from David Fowler in an answer to an issue on GitHub. In short, you need to use the UseSetting extension method on the IWebHostBuilder instance to change the assembly used in the ApplicationName property to point to where the controllers are. In my case this was as follows:
Therefore, without this line redirecting the application name to the correct assembly, if the controllers are not in the same assembly as the Startup class, that's when things go wrong - as I found.
With this problem fixed, everything fell into place and started working correctly.
However, with this up and running, something did not feel right about it.
The solution I had created was fine if all the dependencies are accessible from the new Infra project, either directly within the project or by referencing other projects from the Infra project. But what if I have some dependencies in my MVC project I want to add to the DI container?
This is where my thought experiment broke down. As it stood, it would create a circular reference of the Infra project needing to know about classes in the main MVC project which in turn referenced the Infra project. I needed to go back to the drawing board and think about what I was trying to achieve.
I broke the goal into the following thoughts:
- The main MVC project should not have direct references to projects that provide services other than the Infra project. This is to try to prevent developers from creating instances of classes directly
- Without direct access to those projects, it is not possible for the DI container to register those classes either if the Startup is in the main MVC project
- Moving the Startup and DI container registration to the Infra project will potentially create circular references if classes in the MVC project need to be registered
- Moving the Startup class out of the main MVC project creates a need to change the ApplicationName in the IHostingEnvironment for the controllers to be found
- Moving the Startup class into the Infra project means that the Infra project has to have knowledge of MVC features such as routing etc. which it should not really need to know as MVC is the consumer.
By breaking down the goal, it hit me what is required!
To achieve the goal set out above, a hybrid of the two approaches is needed whereby the Startup and DI container registration remain in the main MVC project, but registration of classes that I don't want to be directly accessed in the MVC project get registered in the Infra project so access in the MVC project is only through interfaces, serviced by the DI container.
To achieve this, all I needed to do was make the Infra project aware of DI registration through the IServiceCollection interface and extension methods, but create a method that has the IServiceCollection injected into it from the MVC project that is calling it.
The first part of the process was to refactor the work I has done in the Startup class in the Infra project and create a public static method to do that work, taking the dependencies from outside.
The new ConfigureServices method takes an IServiceCollection instance for registering services from within the infrastructure project, and also an IMvcBuilder as well, so that any MVC related infrastructure tasks that I want to hide from the main MVC project (and not dependent on code in the MVC project) can also be registered.
In the example above, I add a custom validation filter (to ensure all post-back check if the ModelState is valid rather than this being done in each Action in the MVC controllers) and add the FluentValidation framework for domain validation.
To make things a bit more interesting, I also added an extension method to use Autofac as the service provider instead of the out of the box Microsoft one.
With this in place, a took the Startup class out of the Infra project and put it back into the MVC project and then refactored it so that it would do the following in the ConfigureServices method:
- Perform any local registrations, such as AddMvc and any classes that are in the MVC project
- Call the static methods created in the Infra project to register classes that are hidden away from the MVC project and use Autofac as the service provider.
I ended up with a Startup class that looked like this:
My description of the problem and my solution above only really scratches the surface, but hopefully it is of use to someone. It is probably better to let the code speak for itself, and for this I have created a Git repo with three versions of an example project which show the three different approaches to the problem.
First is the out-of-the-box do everything in the main project
Then there is the refactoring to do all the registration in the Infra project
Lastly, there is the hybrid where the Startup is in the main project, but delegates registration of non-MVC classes to the Infra project.
The example projects cover other things I plan on blogging about, so are a bit bigger that just dealing with separating the Startup class.
The repo can be found at https://github.com/configureappio/SeparateStartup
For details of the projects, look at the Readme.md file in the root of the repo.
In answer to the question posed in the title of this post, my personal view is that the answer is - "No" ... but I do think that extracting out a lot of plumbing away from the Startup into another assembly does make things cleaner and achieves the goal of steering developers away from creating instances of classes and instead, relying on using the DI container to do the work it is intended for. This then helps promote SOLID principles.
Hopefully, the discussion of the trials and tribulations I had in trying to completely move the Startup.cs class show how painful this can be and how a hybrid approach is more suitable.
The underlying principle of using a clean code approach is sound when approached the correct way, by thinking through the actual goal rather than concentrating on trying to fix or workaround the framework you are using.
The lessons I am taking away from my experiences above are:
- I am a big fan of clean architecture, but sometimes it is hard to implement when the frameworks you are working with are trying to make life easy for everyone and make assumptions about your code-base.
- It is very easy to tie yourself up in knots when you don't know what the framework is doing under the bonnet.
- If in doubt, go look at the source code of the framework, either through Git repos or by using the Source Stepping feature of Visual Studio.
- Look at 'what' you are trying to achieve rather than starting with the 'how' - in the case above, the actual goal I was trying to achieve was to abstract the dependency registration out of Startup rather than jumping straight in with 'move whole of the Startup.cs'.