This post is a part of The Second Annual C# Advent. Checkout out all the other blogpost there, or read my entry from the last year on Analyzing unit-ness of white-box tests using OpenCover.
In a usual REST API project, you might decide with your team that you won’t call repository layer directly from controller layer – that there will be a service layer that’s going to reside in-between.
As long as everyone remembers the rule and cares about the rule, everything is great.
The moment someone either forgets, or has a legitimate reason why to abandon the rule, there is nothing in the standard .NET toolset to make them and their codereviewer aware of the fact that the new code does not fit to the architecture as envisioned. .NET namespaces are just naming tools, they do not allow you to formulate visibility rules. .NET types are not up to the task at all.
You can try to place different things into different assemblies and then depend on properly set references, but you probably won’t succeed in capturing the example presented – the hypothetical controller assembly will see repository assembly transitively.
Moreover, every new assembly adds a runtime cost. Why should compile-time checks affect runtime in any way?
In this post, I’m going to introduce an analyzer I’m writing to serve the need. I call it Namespacer. It’s licensed under Apache 2.0 and it’s in early development. I don’t think it’s ready for serious usage, but it makes a good topic for thinking on what can we demand from tools like this.
The premise of the tool is that it’s really important for every member of the team to be able to see and develop the rules. They are not here because architect tries to enforce grand vision on unwilling subjects. They are here so that people are on the same page on what should hold about the codebase right now.
When a new feature requests comes to your desk, it should shed light on whether the initial ideas on code organization are correct or not. Every team member should be able to refine them. That means the license for the associated tooling shouldn’t be expensive and that the format the tooling use for configuration should be merge-friendly.
Namespacer expects you to add it as a NuGet dependency to projects you want to validate. It also needs to have a configuration file called
.namespacer added to the build as “C# analyzer additional file”:
From then on, it will act as a usual Roslyn analyzer: it will throw warnings in thoroughly integrated fashion, i.e. both in IDE and in CI. You are going to be able to change level of reported warnings, to treat them as errors for example.
Do you still remember the example we talked about in the beginning? We can explain the situation to Namespacer using following configuration:
Product => Product: Product.Controllers -> Product.Services Product.Services -> Product.Repositories * -!> *
This says: for every mention (
=>) of something from
Product namespace in something in
Product namespace, the following must apply:
- it’s allowed (
->) to mention
- and it’s also allowed (
->) to mention
- but everything else is not allowed (
As you can see, rules are evaluated in a top-down fashion and order matters. If there is no match in the body of the rule, mention will pass the check.
This config might prove to be too rigid later. One day, you want to add proxy to a new network integration into
Product.Proxy.ANetworkService namespace. You will then find out Namespacer complains:
You should not mention symbol 'Product.Proxy.ANetworkService' in 'Product.Services'. Does it mean that you are supposed to back down from this new notion of proxies? No way! You just need to describe their role in your shared grand plan.
One resolution would be to extend the description:
Product => Product: Product.Controllers -> Product.Services Product.Services -> Product.Repositories Product.Services -> Product.Proxy * -!> *
Another possibility is to redefine the rules towards explicitly forbidding what you don’t want to happen – and allowing everything else:
Product => Product: Product.Controllers -!> Product.Repositories * -> *
Please note that there is a difference between two approaches presented you might have not noticed: when you not include
Product.Services -> Product.Services rule, it’s not possible to mention other services from service code. You are allowed to talk about the class you are in, but not about other class in the tier.
This force the layer to be very simple and pass-through, something you might want to hold in boundary layers (controllers, repositories), but not elsewhere.
Another thing that does not have to strike you at first is that you can use the tool to forbid calls to code that’s not yours.
Let’s say you want to introduce wrapper of
ConfigurationManager that would allow you to control source of configuration better. You will create the wrapper and introduce the new abstraction to all the places in your current codebase where
ConfigurationManager is currently used. Then you will tell your coworkers what you did and they like it. Week later, they are going to use
ConfigurationManager anyway, because humans are not perfect and excitement wears out.
Product => Product: Product.Controllers -!> Product.Repositories * -> * * => System.Configuration.ConfigurationManager: * -!> *
“For calls from everything to
ConfigurationManager, you can’t.”
There is still plenty work left:
- the code could be quicker as I’m currently using semantic model queries too much,
- the expressive possibilities of the configuration language can definitely be extended in various interesting ways,
- and one can always think about ways to state the rules in the C# source code itself.
- Maybe there is some space on refinement on what counts as “mention”,
- and we can totally discuss saneness of my choice of a custom DSL instead of JSON.
Nevertheless, what I’m interested in right now is what people think about usefulness of the whole approach.
I’m sure that “enterprise architects throwing UML diagrams to subordinates” is a thoroughly toxic approach.
On the other hand, I very much like compile-time checks and versioning verifiable truths about the codebase. Ideally, type system should be strong enough to express this by itself, but doing it “from outside”, like Namespacer does, is the second best option.
In my opinion, the whole difference between “evil architecture” and “good architecture” is inclusion and skin in the game. As long as the developers who do the work are the ones to design the rules, all what is left is machine checking that the work is done according to some shared understanding. What’s not to like about that?