a big ball of mud

In Part I of this series we discussed the case for reconsidering the humble monolith (with some structural improvements), now we get to the work of actually implementing this architecture pattern in a .Net Core (.Net 6) Web API project. The final base solution will be published on GitHub

Let’s start by getting organized. Ideally we want to keep our modules organized into folders, by default Visual Studio will already put each project in a solution into its own folder, but it wouldn’t hurt to impose a bit more structure. Here is the proposed folder structure:

.
├── docs
│   └── assets
│   └── readme.md
├── src
│   └── api
│   │   └── SufficentlyAdvanced.Api
│   │       └── SufficientlyAdvanced.Api.Host
│   │       └── SufficientlyAdvanced.Api.Host.Tests
│   │       └── Modules
│   │           └── WeatherForecast
│   │               └── SufficientlyAdvanced.Api.WeatherForecast
│   │               └── SufficientlyAdvanced.Api.WeatherForecast.Tests
│   └── .dockerignore
│   └── readme.md
│   └── SufficientlyAdvanced.sln
│   
|-- readme.md

So this will be our basic folder structure. This keeps modules of the host organized and children of the host monolith, keeping everything necessary for any given module to be self-contained in a parent folder. You’ll also see a docs directory as well as a src directory. You can probably intuit the docs directory is for documentation. Personally, I find it useful to keep docs and code in the same repo; this approach helps me remember to keep both in sync either by manually updating docs or by generating the contents of this directory as part of my pre-commit build process.

There is also a great deal of flexibility for expanding the repo to other types of assets. I have a production application that follows a similar structure and have added folders under src for standalone microservices, Azure functions, e2e tests, etc. I also maintain build and deploy scripts at the root-level of the directory tree. You may shun the monorepo approach so if you extract a module into a stand-alone microservice you may prefer to move this code to a separate repo. As with any hotly debated aspect of our industry it depends.

Let’s get to work creating our project.

Creating Our Solution

Create a new visual studio project. I’m using the ASP.NET Core Web API template targeting .Net 6.0 but I don’t anticipate any issues using 7.0 preview. I’m also going to create a test project for the host although I doubt there will be much to unit test in this particular project. Remember that Visual Studio will create a project directory. In this case we don’t want the project and the solution in the same directory. In my case, I am also enabling docker support and enabling swagger. This tutorial also assumes top-level statements are enabled.

For now we’re going to leave the WeatherForecast controller and service in the host project. We will migrate this controller and logic into it’s own module soon enough.

Our First Test

In our host application, we already have a WeatherForecast controller with a method called GetWeatherForecast and a corresponding RPC-style route. I’m going to write a test that validates this method returns a 200 OK response with a body of some sort. Ideally I’d like my test to fail before it passes, and in fact, my first test will fail since I plan to check for a 200 response code. Currently the default controller implementation simply returns an IEnumerable<WeatherForecast> which won’t cast as an OKObjectResult meaning we can’t check the response code. Implicitly the framework will wrap this response into a JsonObjectResult (which also means content negotiation will be disabled). We’ll fix all of this is due course.

First let’s write our test:

    [TestMethod()]
    public void GetTest()
    {
        var controller = new WeatherForecastController(new NullLogger<WeatherForecastController>());
        var response = controller.Get();
        var okResult = response.Result as OkObjectResult;
        Assert.AreEqual(200,okResult.StatusCode);
        Assert.IsNotNull(response.ToString());
    }

As predicted, we get the following error on our tests:


Test method SufficientlyAdvanced.Api.Host.Controllers.Tests.WeatherForecastControllerTest.GetTest threw exception: 
System.NullReferenceException: Object reference not set to an instance of an object.
    at SufficientlyAdvanced.Api.Host.Controllers.Test.WeatherForecastControllerTest.GetTest() in C:\Users\micha\source\repos\SufficientlyAdvancedWebApi\src\Api\SufficientlyAdvanced.Api\SufficientlyAdvanced.Api.Host.Tests\Controllers\WeatherForecastControllerTest.cs:line 22

If we navigate to the WeatherForecastController.cs we can change the method signature and return value.


        [HttpGet("WeatherForecast")]
        public ActionResult<IEnumerable<WeatherForecast>> Get()
        {
            return new OkObjectResult( Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray());
        }

And our tests should pass. In this case we first changed the return type to a typed ActionResult. We could use a return type of IActionResult however the generated swagger docs will be more sparse when describing expected return types. When the .Net framework returns this, it wraps it as an ObjectResult which, if enabled, will also support content negotiation rather than forcing a response media-type of application/json. You’ll also see I’ve changed the route, I removed the redundant “get” so, in theory, we simply get a collection of WeatherForecast resources. No need for a level 0 system here.

So far so good.

Creating our First Module

Now we create our first module. In this case I’m going to create a .Net 6 Class library called SufficientlyAdvanced.Api.WeatherForecast and place in in the WeatherForecast directory within the modules directory. Once the project is created, we’ll need to add the necessary framework references. For our module, in the package manager console:


PM> Install-Package Microsoft.AspNetCore.Mvc.Core

N.B. If you require access control in your project, you may also need to install the Microsoft.Identity.Web NuGet package.

We’re also going to create an MSTest project and add a reference to back to our module project.

Within our WeatherForecast module, create a directory called “Controllers” and let’s move our WeatherForecastcontroller.cs into that standalone module. We’ll also need to remove our class WeatherForecast.cs over. I’m going to create a new directory in the root of the module called ResourceModels and place the class in there.

Finally I am going to migrate our test to the stand-alone module’s test project and ensure everything still passes.

Since we don’t have any integration tests yet let’s run the host application and see if it has detected our remote controller. If you have swagger enabled, you should see your recently relocated controller available in the online documentation.

Make Everything Internal

As mentioned in Part I With a modular monolith, we aim to reproduce some of the same firm module boundaries we might find in a stand-alone microservice. To accomplish this, we adopt the practice to make everything internal. This ensures only code within the module may access its functionality. Of course, we will be crossing that assembly boundary in our test projects, so we will be using the [InternalsVisibleToAttribute] annotation.

If we go into the WeatherForecast.Api project and change the access modifier to internal and run the project, the controller will disappear from swagger. Asp.Net Core expects controllers to always be public. We’re going to need to modify our host application to support internal controllers. We do this by implementing and registering a custom ControllerFeatureProvider which will be used by Asp.Net Core to determine if a given type is a controller or not.

In our host we create a new file, InternalControllerFeatureProvider.cs with the following contents:


public class InternalControllerFeatureProvider : ControllerFeatureProvider
{
    protected override bool IsController(TypeInfo typeInfo)
    {
        var isCustomController = !typeInfo.IsAbstract
                    && typeof(ControllerBase).IsAssignableFrom(typeInfo)
                    && IsInternal(typeInfo);
        return isCustomController || base.IsController(typeInfo);

        bool IsInternal(TypeInfo t) =>
            !t.IsVisible
            && !t.IsPublic
            && t.IsNotPublic
            && !t.IsNested
            && !t.IsNestedPublic
            && !t.IsNestedFamily
            && !t.IsNestedPrivate
            && !t.IsNestedAssembly
            && !t.IsNestedFamORAssem
            && !t.IsNestedFamANDAssem;
    }
}

Next we open Program.cs to call our new FeatureProvider. Since I don’t anticipate putting any controllers directly in the host I’m going to replace this line:


builder.Services.AddControllers();

with this line:


builder.Services.AddControllers().ConfigureApplicationPartManager(manager =>
{
    // Clear all auto-detected controllers.
    manager.ApplicationParts.Clear();

    // Add feature provider to allow "internal" controller
    manager.FeatureProviders.Add(new InternalControllerFeatureProvider());
});

Conclusion

In third and final installment in this series, we look at the steps to extract a module from this monolith to form a standalone microservice.