Analyzer that checks references from code to different namespaces in .NET, based on your own rules
┌───┐ ┌─┐ ┌───┐
│┌─┐│ │┌┘ │┌─┐████
│└─┘├──┬┘└┬──┬─┬──┬─┐┌──┬──┤│░└█══█──┐
│┌┐┌┤│─┼┐┌┤│─┤┌┤│─┤┌┐┤┌─┤│─┤│░┌█══█┌┐│
│││└┤│─┤││││─┤│││─┤│││└─┤│─┤└─┘█══█└┘│
└┘└─┴──┘└┘└──┴┘└──┴┘└┴──┴──┴───█══█┌─┘
██││
└┘
If you are working on a code base with multiple developers and you have some ideas about how dependencies must flow, it is a good idea to translate those ideas to a set of rules. But checking those rules manually when changes are made can be cumbersome and error-prone.
ReferenceCopAnalyzer allows you to define those rules in a separate file and will report anything in your code that violates the rules. It uses a code analyzer, which runs as part of the compilation process. That makes it easy to install, fast and well-integrated into your development tooling (IDE). Also, it requires no extra effort to setup in your CI build.
Here's an example of a rule that is defined and a violation being reported in Visual Studio:
This project is heavily inspired by NsDepCop, but a bit easier to use and it supports .NET Core (and .NET 5+).
- Install the NuGet package in your project. Run the following command in the NuGet package manager console
Install-Package ReferenceCopAnalyzer
or using the .NET cli
dotnet add package ReferenceCopAnalyzer
- Add a file called
.refrules
anywhere in your project and ensure that it is registered as anAdditionalFile
in your project file.
<ItemGroup>
<AdditionalFiles Include=".refrules" />
</ItemGroup>
Alternatively, set C# analyzer additional file
as the build action on the file properties in Visual Studio.
You may notice errors already popping up. This is because there are no rules defined yet. Please proceed to the next section.
The contents of the .refrules
file will be used by the analyzer to determine how dependencies are allowed to flow. The following are some examples to help you get started defining those rules.
The file format is inspired by .gitignore
files. However, any dependencies that do not match any of the rules, will get reported.
Here is an example .refrules
file that will allow any reference. This is not very useful, obviously, but it's a good way to get started.
# Allow any reference to and from anywhere
* *
# Allow any class in any namespace to reference System
* System
If you also want to allow references to namespaces like System.Text.Json
or System.Threading
, you can add a wildcard.
# Allow any class in any namespace to reference System or any deeper namespace
* System
* System.*
You can use !
to make a rule that disallows a reference.
# Allow any class in any namespace to reference System or any deeper namespace
* System
* System.*
# But don't allow references to System.IO or anything deeper
!* System.IO
!* System.IO.*
Rules are processed in order, so the last rule that matches a reference determines if the reference is allowed. So, for example, if you would amend the previous example with...
* System.*
... then references to System.IO
would still be allowed.
Given a setup where you have a namespace MyProject.Controllers
and a namespace MyProject.Services
, you may want to ensure that controllers can reference services (and not the other way around).
# Controllers can access services, but not the other way around
MyProject.Controllers MyProject.Services
You can also generalize this rule, so that it can be used in multiple projects. This can be done using "named wildcards".
# Controllers can access services, but not the other way around
[project].Controllers [project].Services
In this example, [project]
can be anything, but it has to be the same for the source ([project].Controllers
) and the target ([project].Services
) of the rule. So a reference from One.Controllers
to One.Services
will be allowed, but a reference from One.Controllers
to Two.Services
will not be.
The example below shows an example that could be a starting point for a MVC project.
# Anything can reference System, e.g. for using Console.WriteLine(...)
* System
# Services can reference things under the Data namespace, e.g. for DB or DTO classes.
# And anything under System.* for doing things like web requests and serialization.
[project].Services [project].Data
[project].Services System.*
# Controllers blend things together, so they can access services, models and data.
[project].Controllers [project].Data
[project].Controllers [project].Models
[project].Controllers [project].Services
If you are familiar with Sitecore Helix architecture, this may be a good starting point for setting up rules for that. Note that this can be used regardless of whether you are using a 'module is a .csproj' approach or not.
# Dependencies flow from project to feature to foundation
[ca].Project.* [ca].Feature.*
[ca].Feature.* [ca].Foundation.*
# Foundation projects are allowed to reference each other
[ca].Foundation.* [ca].Foundation.*
# Controllers are allowed to reference models
[ca].Feature.[f].Controllers [ca].Feature.[f].Models
[ca].Foundation.[f].Controllers [ca].Foundation.[f].Models
# Any module is allowed to reference their own root namespace
[ca].Feature.[f].* [ca].Feature.[f]
[ca].Foundation.[f].* [ca].Foundation.[f]
[ca].Project.[f].* [ca].Project.[f]
You may run into "false positives" for "using" directives that are not actually used. ReferenceCopAnalyzer will still report them, if they violate the rules.
But since they aren't needed anyway, why not just remove them? And IDE0005 reports them (this analyzer comes with .NET 5+ by default, or can be installed separately if needed).
If you really want to keep things clean, just set IDE0005 severity to 'error'. You just need to add a .editorconfig
file and include the following:
# IDE0005: Using directive is unnecessary.
dotnet_diagnostic.IDE0005.severity = error
It's much easier to manage rules for a whole solution than it is to manage rules for individual projects. But you do have to include the .refrules
file in all the individual projects.
The easiest way to do this:
- Right-click the root item in the solution explorer in Visual Studio and select "Manage NuGet Packages for Solution..."
- Find the
ReferenceCopAnalyzer
package - In the right pane, select all projects and click "Install"
- Locate the solution folder on the filesystem and create a folder with the name
rules
- Add a
.refrules
file to the folder (initially, the content of the file could just be* *
) - Add the following to every
.csproj
file in the solution:
<ItemGroup>
<AdditionalFiles Include="$(SolutionDir)\rules\.refrules" />
</ItemGroup>
If it fits your solution, you could use Directory.Build.props instead. It's a bit trickier to setup initially, but should be easier to maintain if/when new projects are added to your solution.
- Place the
.refrules
file in a separate folder - Go to your repository and setup a branch policy
- Under
Automatically included reviewers
add a new reviewer policy - Configure the policy to add a specific reviewer if the folder has changes
Example: