Dependency Rules in Vertical-Sliced .NET projects
Let’s say I’m building a Library codebase, with domains: Books, Readers, and Borrows.
Clean architecture advocates I structure my project like:
- Library.Domain
- Book.cs
- Reader.cs
- Borrow.cs
- Library.Application
- BookService.cs
- ReaderService.cs
- BorrowService.cs
- Library.Infrastructure
- BookStore.cs ...
- Library.WebApi
- BookController.cs ...
where each of the above is its own .NET project.
Vertical-sliced architecture advocates for grouping by features instead of type, so the above structure becomes:
- Library
- Books
- BookStore.cs
- BookService.cs
- Book.cs
- Readers
- ReaderStore.cs
- ReaderService.cs
- Reader.cs
- Borrows
- BorrowStore.cs
- BorrowService.cs
- Borrow.cs
where there is a single .NET project with sub-folders, each representing a domain of the business problem being solved, and each having files for the different types.
Why does clean-architecture ask me to have different projects? It’s because of the Dependency Rule.
Library.WebApi -> Library.Infrastructure -> Library.Application -> Library.Domain
The WebApi project depends on the Infrastructure project as above, and so on, because I don’t want the Model/Entities code in the Domain layer to make reference to Database Storage code in the Infrastructure layer, etc.
And this makes sense.
If the Domain and Infrastructure code are in the same project, then I could make the mistake of breaking the Dependency Direction rule.
So how can we keep the dependency rule in when structuring a single .NET project by Vertical-sliced architecture?
TL;DR; Use the Rosyln compiler to enforce the dependency rule on the namespace level, instead of the project level.
I use NsDepCop
I personally have found success with a tool called NsDepCop. It lets Me specify a config.nsdepcop
XML file, where I can write rules for whether namespaces are allowed to reference each other.
Unlike clean-architecture, that relies on the segregation of .NET projects to enforce dependency direction rules, using nsdepcop relies on the Rosyln compiler to enforce whether namespace1 can import code from namespace2.
How I assign namespaces
Namespaces in .NET are very important and quite underused in business code IMO. They are a logical grouping of related code, and in vertical-sliced architecture, I can use them to the fullest:
- Entity/Model class files like
Book.cs
,Reader.cs
andBorrow.cs
will be under namespace:Library
- Service/Application class files like
BookService.cs
,ReaderService.cs
andBorrowService.cs
will be under namespaces:Library.Books
,Library.Readers
andLibrary.Borrows
, respectively. - Store/Repository/Persistence class files will be under namespaces:
Library.Books.Store
,Library.Readers.Store
,Library.Borrows.Store
, respectively.
This means that with using Library
, I have access to all models in the system, and so on.
And then I’d have a config.nsdepcop XML file like:
<NsDepCopConfig IsEnabled="true" ParentCanDependOnChildImplicitly="true">
<Allowed From="*" To="System.*" />
<!-- allow the service/application layer to reference the models/entities -->
<Allowed From="Library.?" To="Library" />
<!-- allow the persistence layer to reference the models/entities -->
<Allowed From="Library.?.Store" To="Library" />
<!-- foreach service below, let's only expose the public interfaces -->
<Allowed From="Library.*" To="Library.Books">
<VisibleMembers>
<Type Name="IBookService" />
</VisibleMembers>
</Allowed>
<Allowed From="Library.*" To="Library.Readers">
<VisibleMembers>
<Type Name="IReaderService" />
</VisibleMembers>
</Allowed>
<Allowed From="Library.*" To="Library.Borrows">
<VisibleMembers>
<Type Name="IBorrowService" />
</VisibleMembers>
</Allowed>
</NsDepCopConfig>
This way, the dependency rules are maintained, making each subfolder its own clean architecture ecosystem.
This is Vertical-sliced architecture!
You only need one project. You could split it out when it makes sense, and because of the namespaces, it’s quite easy to do so, but you’re not forced to do this.
Whether you agree with my project structure or not, I hope you at least now know that:
- you can have a single project with enforced dependency rules,
- and how to enforce your own rules.
I hope to see more projects where code is grouped by feature instead of by type.