Clean Architecture and Transitive Dependencies in .NET Core

January 29, 2022

If you are familiar with the concept of Clean Architecture, popularized by Uncle Bob, you know that the Domain layer is at the core of the application, only referenced by the Application layer. The Application layer is responsible for the business logic of the application, and the Domain layer is responsible for the logic of the domain (that is, agnostic from the needs of the specific use cases of this app, and always true in regard to your business).

Other layers include Infrastructure and Presentation. The Infrastructure layer is responsible for the data access (database, web services, etc.), and the Presentation layer is responsible for the presentation of the data (web API, desktop, etc.). These layers interact with the Domain layer through the Application layer, never by directly calling the domain objects. This preserves encapsulation, and makes the application more testable as well. Of course, that makes the Application layer responsible for mapping the domain objects into DTOs, usable by external layers, and the other way around.

The issue though, is that nothing prevents external layers from directly invoking domain objects, except developers’ discipline. This is aggravated by the fact that .NET Core makes dependencies transitive by default, meaning that any layer referencing the Application layer will also be referencing the Domain layer, and will be able to access anything public. Now, transitive dependencies a not a bad thing per se, and they are actually super useful when reusing NuGet packages across projects for example, but in this case we may want to disable that behavior.

Let’s create a simple example application to illustrate. It will contain a console project (the Presentation), and two class libraries (the Application and Domain layers). The Domain contains a single class, RandomAggregate (note the usage of file-scoped namespaces, so cool):

namespace Domain;

public class RandomAggregate
{
    public void DoSomething() { }
}

Same for the Application layer, a single class RandomHandler:

using Domain;

namespace Application;

public class RandomHandler
{
    public void DoSomething()
    {
        var aggregate = new RandomAggregate();
        aggregate.DoSomething();
    }
}

And now the Presentation layer, a simple console application:

using Application;
using Domain;

// I call the handler, which in turn calls the aggregate
var handler = new RandomHandler();
handler.DoSomething();

But what if we try calling the aggregate directly?

using Application;
using Domain;

// I call the handler, which in turn calls the aggregate
var handler = new RandomHandler();
handler.DoSomething();

// nothing prevents me from calling the aggregate directly!
var aggregate = new RandomAggregate();
aggregate.DoSomething();

Because the Domain dependency in Application is transitive, Presentation is able to use the domain object directly. Let’s see how to disable this behavior.

At the moment, the Application project file looks like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\Domain\Domain.csproj" />
  </ItemGroup>

</Project>

Let’s update the Application project file to disable transitive dependencies:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
	  <ProjectReference Include="..\Domain\Domain.csproj">
		  <!-- this is the important line -->
          <PrivateAssets>All</PrivateAssets> 
	  </ProjectReference>
  </ItemGroup>

</Project>

And now .NET will complain that it cannot find the type or namespace Domain and RandomAggregate. Bingo! Our Domain is now isolated inside the Application layer, forcing the use of mapped types, and preventing IntelliSense from being polluted.

One could argue that marking all types inside Domain as internal and adding InternalsVisibleTo would achieve a similar purpose, but in my opinion the meaning is quite different. The internal keyword is used to mark types that are not meant to be used by other layers, and the InternalsVisibleTo attribute must be put in the same assembly, thus requiring Domain to have some (limited) knowledge of other layers (Application in this case). By leaving everything public, and then disabling the transitive dependency, it is actually Application which decides what to share. Domain remains unaware of which layers use it.


Profile picture

Written by Jonathan Hiben who lives in Belgium and works in Luxembourg building useful things in the cloud.