Clean Architecture – Should I Move the Startup Class to Another Assembly?

I was recently listening to an episode of the brilliant .Net Rocks where Carl and Richard were talking to Steve Smith (a.k.a @ardalis) in which he talks about clean architecture in ASP.Net Core.

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?

404 when MVC controller not found
404 when MVC controller not found

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:

UseSetting(WebHostDefaults.ApplicationKey, typeof(Program).GetTypeInfo().Assembly.FullName)

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:

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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.

Startup Separation

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:

The Full Example

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.

Conclusion

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’.

5 thoughts on “Clean Architecture – Should I Move the Startup Class to Another Assembly?

  1. Hi Steve,
    By moving the Startup class to another project, you don’t need to have a direct reference to the “implementation” projects inside the Web project. But still, you will add them all to the new “infra” project, correct?

    Maybe I didn’t get it here, what’s the added value by doing this? What gains the Web project would have?

    Many thanks
    Bilal

    1. Hi Bilal. Thanks for the comment.

      I know Steve Smith has already responded to you on Twitter, but I thought I would reply here as well.

      As Steve said on Twitter, the idea is not to add value to the web project, but is more about applying constraints to the project to prevent a developer from instantiating classes from other projects directly.

      Instead, the MVC project is only aware of two dependency projects:

        the project that defines common interfaces and abstracts (with no instantiable classes)
        the Infra project which again, should not have any publicly instantiable classes. Instead, the only class should be a static class with static methods to perform any Di container registration.

      This forces the developer to rely on the DI container to resolve instances for interfaces and abstracts.

      Hope this is of help.

      Steve

  2. I agree entirely that `Startup` all to easily becomes a “God” class and needs careful consideration and refactoring

    Personally, I hate static helper classes (like your `ServiceConfiguration` helper) because they’re just moving the code to another file without necessarily providing any more structure. Likewise moving service registration into separate methods doesn’t necessarily provide the architectural separation you’re after – when another developer comes along later to make a change, how do they know where to add a new registration?

    So this is an nteresting and very similar approach to the same issue I’ve addressed recently with my “magic” DI bootstrapper library (https://github.com/Rammesses/IoC.Discovery).

    1. Hi Joel. Thanks for the comment.

      I had a read of your blog about the ‘Magic’ discovery. Very cool. When I can find some time, I’ll have a look at the code from GitHub.

      I think we are approaching slightly different problems with the two posts.

      The point of my post was not so much about moving away from Startup being a ‘God’ class, but was intended to be more of a thought experiment in applying a constraint to the solution so that all projects (other than the project registering classes with the DI container) should not have any knowledge of concrete classes in the other projects.

      If written correctly, all projects (other than the MVC project) should only have dependencies on common interface definitions.

      However, because the out-of-the-box MVC project is responsible for registering all dependencies, it has to have references to all the other projects which means that any public concrete classes in those projects have the potential to be directly instantiated by a developer rather than relying on the DI container to obtain an implementation of the interface.

      By taking the responsibility for DI registration away from the MVC project and moving it to a ‘hub’ or ‘glue’ project (the Infra project in my example solution), the intention is to leave the MVC project ignorant of the other projects’ concrete classes and therefore prevent a developer in that project from being able to instantiate those classes directly.

      With this model in mind, the only publicly accessible class and methods in the Infra project should be solely for the purpose of DI container registration.

      I think ultimately, we both achieve the same end result, but with mine being declarative in the Infra project and yours being dynamic via the Bootstrapper discovery.

      With regards to the static helper method, I originally intended it to be an extension method for IServiceCollection. I changed my mind while developing as it needed the second parameter of IMvcBuilder. I agree, from a SOLID perspective, I usually avoid static methods like the plague, but in this case, as the purpose of the method is just plumbing and not really in need of being mockable or substitutable, there seemed little benefit in having an instance class in this case.

      Thanks

      Steve

Leave a Reply

Your email address will not be published. Required fields are marked *