NsDepCop Help

July 20, 2026 ยท View on GitHub

Supported project types

  • Projects with a csproj project file are supported.
  • .Net Core and .Net 5+ projects are supported in v2.0 or above.
  • Projects with an xproj project file are not supported.

Dependency rules

  • Allowed and disallowed namespace and assembly dependencies are described with dependency rules in config files.
  • The rule config file must be named config.nsdepcop and its build action must be set to C# analyzer additional file (the NsDepCop NuGet package sets it automatically).
  • The default (and recommended) approach is allowlisting, that is, if a dependency is not explicitly allowed then it is disallowed. (See also: denylisting).
  • The config file can inherit other config files from parent folders, see config inheritance.
  • Assembly dependency checking is disabled by default (for backward compatibility reason) and needs to be enabled with CheckAssemblyDependencies attribute on the root element. See Example Two.
  • Rules can specify namespaces in the following ways.
Namespace specification typeExample
ExactSystem.IO
WildcardSystem.Collections.*
MyProduct.?.StorageModel
Regex/MyProduct(.[\w]+)*.StorageModel[\w]/
PlaceholderMyProduct.[Module].StorageModel
See Placeholders.

Notice that Regex patterns must be enclosed in forward slashes ('/').

Rule notation

NotationMeaning
. (a single dot)The global namespace.
* (a single star)Any namespace.
MyNamespace.*MyNamespace and any sub-namespaces.
MyNamespace.?All direct sub-namespaces of MyNamespace
*.MyNamespaceAny namespace named MyNamespace
?.MyNamespaceAny namespace named MyNamespace which has exactly one parent namespace.
MyNamespace.*.MyOtherNamespaceAny namespace called MyOtherNamespace which has an ancestor named MyNamespace
MyNamespace.?.MyOtherNamespaceAny namespace called MyOtherNamespace which has a grandparent named MyNamespace
/RegexPattern/Any namespace that matches the specified regular expression (Regex) pattern.
The pattern must be enclosed in forward slashes ('/').
Regex special characters must be escaped, e.g. '.' should be "\.". See the Regex reference for details.

Example One

<NsDepCopConfig IsEnabled="true" ChildCanDependOnParentImplicitly="true">
    <Allowed From="*" To="System.*" />
    <Allowed From="NsDepCop.*" To="Microsoft.CodeAnalysis.*" />
    <Allowed From="NsDepCop.ParserAdapter.Roslyn" To="NsDepCop.Analysis" />
</NsDepCopConfig>

Meaning:

  • Any namespace can reference the System namespace and any of its sub-namespaces.
  • The NsDepCop namespace and all of its sub-namespaces can reference the Microsoft.CodeAnalysis namespace and any of its sub-namespaces.
  • The NsDepCop.ParserAdapter.Roslyn namespace can reference the NsDepCop.Analysis namespace (but not its sub-namespaces).

Example Two

<NsDepCopConfig IsEnabled="true" CheckAssemblyDependencies="true">
    <AllowedAssembly From="*" To="*" />
    <DisallowedAssembly From="*.Repository" To="*.Service" />
</NsDepCopConfig>

Meaning:

  • Any assembly can reference each other with the following exception.
  • The Repository layer cannot reference the Service layer.

Tip: When using the allowlisting approach for assemblies, don't forget the include the following rules:

<NsDepCopConfig IsEnabled="true" CheckAssemblyDependencies="true">
    <AllowedAssembly From="*" To="mscorlib" />
    <AllowedAssembly From="*" To="netstandard" />
    <AllowedAssembly From="*" To="System.*" />
</NsDepCopConfig>

Config attributes

You can set the following attributes on the root element. (Bold marks the the default value.)

AttributeValuesDescription
IsEnabledtrue, falseIf set to false then analysis is not performed for the project.
ChildCanDependOnParentImplicitlytrue, falseIf set to true then all child namespaces can depend on any of their parents without an explicit allowing rule. The recommended value is true. (False is default for backward compatibility.)
ParentCanDependOnChildImplicitlytrue, falseIf set to true then all parent namespaces can depend on any of their children without an explicit allowing rule. The recommended value is false.
MaxIssueCountint (>0), default: 100Analysis stops when reaching this number of dependency issues.
AutoLowerMaxIssueCounttrue, falseIf set to true then each successful build yielding fewer issues than MaxIssueCount sets MaxIssueCount to the current number of issues.
InheritanceDepthint (>=0), default: 0Sets the number of parent folder levels to inherit config from. 0 means no inheritance.
ExcludedFilesComma separated list of file patternsDefines which source files should be excluded from the analysis. Paths are relative to the config file's folder. E.g.: **/*.g.cs,TestFiles/*.cs
CheckAssemblyDependenciestrue, falseWe adopt the 'disallowed-by-default' approach for assembly dependencies check, similar to how we handle namespace dependencies (where everything is disallowed unless explicitly permitted). To ensure the backward compatibility, this configuration attribute has been introduced to explicitly enable the assembly dependency checking. By default this attribute is false.

Allowlisting

  • The <Allowed From="N1" To="N2"/> config element defines that N1 namespace can depend on N2 namespace.
  • If a dependency does not match any of the allowed rules then it's considered disallowed.

Examples:

ExampleMeaning
<Allowed From="MyNamespace" To="System" />MyNamespace can depend on System
<Allowed From="MyNamespace" To="System.*" />MyNamespace can depend on System and any sub-namespace
<Allowed From="MyNamespace" To="*" />MyNamespace can depend on any namespace
<Allowed From="MyNamespace" To="." />MyNamespace can depend on the global namespace
<Allowed From="MyNamespace" To="System.*.Serialization.*" />MyNamespace can depend on all Serialization namespaces in System and their sub-namespaces

Denylisting

  • The <Disallowed From="N1" To="N2"/> config element defines that N1 namespace must not depend on N2 namespace.
  • To implement the denylisting behavior, you also have to define an "allow all" rule, otherwise no dependency will be allowed.
  • Only those dependencies are allowed that has a matching "Allowed" rule and no match with any of the "Disallowed" rules.
  • You can specify any number of "Allowed" and "Disallowed" rules in any order.
  • If both an "Allowed" and a "Disallowed" rule are matched then "Disallowed" is the "stronger".

Example:

<NsDepCopConfig>
    <Allowed From="*" To="*" />
    <Disallowed From="MyFrontEnd.*" To="MyDataAccess.*" />
</NsDepCopConfig>

Meaning:

  • Every dependency is allowed but MyFrontEnd (and its sub-namespace) must not depend on MyDataAccess (and its sub-namespaces).

Behavior of the wildcards '*' and '?'

If any Disallowed rule matches, no Allowed rule is considered.

If multiple Allowed rules match the same namespace, the one with best matching From rule is selected.

The best matching rule is the one with the minimal edit distance between namespace pattern and namespace name. The edit distance is calculated as the sum of all edit operations which are needed to replace the wildcards with the namespace names. The costs are as follows:

  • Replacing a ? has a cost of 1.
  • Replacing a * has a cost of 1 and additionaly a cost of 1 per sub-namespace that replaces the *.

Example: When matching the namespace A.B.C.D the rule A.?.?.D (edit distance = 2) is preferred to the rule A.*.D (edit distance = 3). If multiple rules have the same edit distance, the behavior is undefined.

Placeholders

  • Placeholders link the From and the To side of a rule: a placeholder captures namespace components on the From side, and the To side refers to the captured value.
  • This makes rule sets module-agnostic. Say each component's Services may depend only on its own Domain:
A.Services -> A.Domain
B.Services -> B.Domain
C.Services -> C.Domain

Instead of one rule per component (which has to be extended whenever a component is added), a single rule expresses all of them:

<Allowed From="[Module].Services" To="[Module].Domain" />

Wildcards cannot express this because the two sides match independently: <Allowed From="?.Services" To="?.Domain" /> would also allow A.Services -> B.Domain. The same applies to Regex rules: the two patterns are evaluated in separate matches, so a capture on the From side cannot be referenced on the To side.

NotationMeaning
[Name]Matches and captures exactly one namespace component (like ?).
[Name*]Matches and captures one or more namespace components (like *, but at least one).
[!Name]Valid only on the To side. Matches exactly one component that is NOT equal to the value captured for Name on the From side.
  • How a From-side pattern matches and captures:
PatternNamespaceMatch?Captured value(s)
[M].CA.CyesM = A
[M].CA.B.Cno ([M] matches exactly one component)
[M*].CA.B.CyesM = A.B
[M*].CCno ([M*] matches at least one component)
App.[M].*App.A.B.CyesM = A
[M].[N*].DA.B.C.DyesM = A, N = B.C
  • On the To side the captured value replaces the placeholder before matching. With the rule <Allowed From="[Module].Services" To="[Module].Domain.*" /> the dependency A.Services -> A.Domain.Entities matches (the To side becomes A.Domain.*), while A.Services -> B.Domain.Entities does not.
  • Multiple placeholders can be combined. Example: allow every component to use a shared area, but only within the same layer:
<Allowed From="App.[Module].[Layer].*" To="App.Shared.[Layer].*" />
  • A multi placeholder captures a whole sub-path as one value, which covers nested components: <Allowed From="App.[Path*].Services" To="App.[Path*].Domain" /> allows App.A.B.Services -> App.A.B.Domain (captured value: A.B) but not -> App.A.Domain: the complete captured sub-path must reappear on the To side.
  • A negated placeholder expresses inequality and is typically used in Disallowed rules. The following rule forbids using another component's Domain, even if a broad Allowed rule (eg. inherited from a parent config) would otherwise permit it:
<Disallowed From="[Module].*" To="[!Module].Domain.*" />
  • Placeholders are bound by name, not by position: the To side may reference them in any order (eg. From="App.[P1].[P2].*" To="App.[P2].[P1].*" swaps the two segments), repeat a name, or use only a subset of the From side's placeholders.
  • A placeholder that appears only on the From side is valid; the captured value is simply unused. It still matches like the corresponding wildcard ([Name] like ?, [Name*] like *), except that [Name*] keeps requiring at least one component โ€” it never matches zero, regardless of the To side. The reverse is not valid: see below.
  • Placeholders can be mixed with the ? and * wildcards; those match as usual but do not capture.
  • Validity rules (violations are reported as a config error):
    • Placeholder names on the From side must be unique.
    • Every placeholder on the To side (including negated ones) must also occur on the From side.
    • [!Name] must not refer to a multi placeholder ([Name*]), and [!Name*] is not valid.
    • Two adjacent multi-matching tokens (* or [Name*]) are not allowed, mirroring the rule that forbids *.*.
  • When several capture lengths are possible (eg. [A*].[B] matching X.Y.Z), the leftmost placeholder captures as few components as possible; there is no backtracking over alternative splits against the To side. Place a literal component between multi-matching tokens to avoid the ambiguity altogether.
  • When selecting the best matching rule, a placeholder counts like the corresponding wildcard: [Name] like ?, [Name*] like * (see the edit distance calculation above).
  • Placeholders can also be used in assembly dependency rules.

Notice that a namespace specification containing '[' is interpreted as a placeholder specification, unless it is enclosed in forward slashes ('/'), in which case it is a Regex.

Namespace surface

  • The surface of a namespace consists of the types that are visible to some other namespace.
  • The <VisibleMembers> config element defines the surface of a namespace.
  • In the following example GameLogic can use only Vector2 and Vector3 types of the UnityEngine namespace.
<Allowed From="GameLogic" To="UnityEngine">
    <VisibleMembers>
        <Type Name="Vector2" />
        <Type Name="Vector3" />
    </VisibleMembers>
</Allowed>
  • Notice that the surface is defined in the context of a particular namespace dependency, that is, this surface of UnityEngine is accessible only to GameLogic.
  • You can define different surfaces for different other namespaces.
  • You can also define a "global" surface, that is, a surface that is applicable to all namespaces that otherwise are allowed to depend on UnityEngine. See the following example.
<VisibleMembers OfNamespace="UnityEngine">
    <Type Name="Vector2" />
    <Type Name="Vector3" />
</VisibleMembers>
  • Notice that when defining a "global" surface the <VisibleMembers> element is not embedded in an <Allowed> element but you must specify the OfNamespace attribute.

Allowing all child namespaces to depend on their parents

You can specify the ChildCanDependOnParentImplicitly attribute on the NsDepCopConfig element.

  • True means that all child namespaces can depend on any of their parent namespaces without requiring an explicit Allowed rule.
  • True is in line with how C# type resolution works: it searches parent namespaces without requiring an explicit using statement.
  • False means that all dependencies between children and their parents must be explicitly allowed with a rule.
  • False is the default for backward compatibility.

Example:

<NsDepCopConfig ChildCanDependOnParentImplicitly="true">
    <!-- The following rule is not necessary because the ChildCanDependOnParentImplicitly="true" attribute implies it. -->
    <Allowed From="MyNamespace.SubNamespace" To="MyNamespace" />
</NsDepCopConfig>

Allowing all parent namespaces to depend on their children

You can specify the ParentCanDependOnChildImplicitly attribute on the NsDepCopConfig element.

However, this is not recommended, because child namespaces are usually more concrete/specialized than their parents and the dependecies should point from the more concrete/specialized to the more abstract/generic and not the other way.

Config inheritance

From v1.6 NsDepCop supports config inheritance, aka multi-level config.

  • The goal is to achieve "DRY" configs, that is, avoid redundant info in config.nsdepcop files.
  • You can extract common config settings from project-level config.nsdepcop files and put them into a "master" config file, that must be in a folder that is a common ancestor of the project folders, e.g. the solution folder.
  • The "master" config file must also be named config.nsdepcop.
  • You have to "turn on" inheritance in the project-level configs by setting the InheritanceDepth attribute to a number that indicates the number of folder levels between the project folder and the master config's folder.
  • Typically you put the master config file into the solution folder which is the immediate parent of the project folders, so you set InheritanceDepth="1" in all project-level configs.

Example:

config.nsdepcop file in "C:\MySolution":

<NsDepCopConfig ChildCanDependOnParentImplicitly="true">
    <Allowed From="*" To="System.*" />
    <Allowed From="*" To="MoreLinq" />
    <Allowed From="*" To="NsDepCop.Core.Util" />
</NsDepCopConfig>

config.nsdepcop file in "C:\MySolution\MyProject":

<NsDepCopConfig InheritanceDepth="1">
    <Allowed From="NsDepCop.MsBuildTask" To="NsDepCop.Core.Interface.*" />
    <Allowed From="NsDepCop.MsBuildTask" To="NsDepCop.Core.Factory" />
    <Allowed From="NsDepCop.MsBuildTask" To="Microsoft.Build.*" />
</NsDepCopConfig>

More info:

  • If there is a conflict between the project-level and the inherited settings then the project-level settings "wins".
  • The IsEnabled attribute has different meaning in the project-level config and in inherited configs.
    • If IsEnabled="false" in a project-level config then the project don't get analyzed.
    • If IsEnabled="false" in an inherited config then its content doesn't get inherited.
  • The inheritance is not limited to just a project and a solution level config; you can have any number of config.nsdepcop files at any folder levels. Just make sure you set the InheritanceDepth to a number that is great enough to find all the higher-level configs.
  • There must always be a config.nsdepcop file in the project folder if you want to analyze that project. Even if all the settings come from a higher-level config, you have to put at least a minimal config to the project level, that enables the inheritance in the first place. E.g.: <NsDepCopConfig InheritanceDepth="3"/>

Dealing with a high number of dependency issues

If there are so many dependency issues that you cannot fix them all at once but you still want to control them somehow then try the following.

  • Prevent the introduction of more dependency issues. Set the current number of issues as the maximum and make it an error to create more.
<NsDepCopConfig MaxIssueCount="<the current number of issues>">
  • Encourage developers to gradually fix the dependency issues by automatically lowering the max issue count whenever possible. Turn on AutoLowerMaxIssueCount.
<NsDepCopConfig AutoLowerMaxIssueCount="true" MaxIssueCount="<the current number of issues>">

Please note that when NsDepCop modifies the nsdepcop.config files their formatting will be reset (because of the XML deserialization/serialization roundtrip).

Disabling with an environment variable

To disable the tool globally, set the DisableNsDepCop environment variable to true or 1.

setx DisableNsDepCop 1

It will affect both MSBuild integration (NuGet package) and Visual Studio integration (VSIX package).

Note that it won't affect processes that are already running, only the newly started ones.

Config XML schema

See the XSD schema of config.nsdepcop here.

Config XML schema support in Visual Studio

Add NsDepCop config XML schema to the Visual Studio schema cache to get validation and IntelliSense when editing NsDepCop config files.

  • Copy the following files into the Visual Studio schema cache folder, located at <VsInstallDir>/Xml/Schemas (eg. C:\Program Files\Microsoft Visual Studio\2022\Community\Xml\Schemas):

v1.x only topics

The following topics apply only to v1.x versions.

Config attributes deprecated in v2.0

AttributeValuesDescription
CodeIssueKindInfo, Warning, ErrorDependency violations are reported at this severity level.
InfoImportanceLow, Normal, HighInfo messages are reported to MSBuild at this level. This setting and the MSBuild verbosity (/v) swicth together determine whether a message appears on the output or not. See Controlling verbosity for details.
MaxIssueCountSeverityInfo, Warning, ErrorThis is the severity of the issue of reaching MaxIssueCount.
AnalyzerServiceCallRetryTimeSpansComma separated list of wait times in milliseconds, default: 100, 300, 1000, 3000, 10000These wait times are used between retries when the NsDepCop MsBuild Task cannot communicate with the out-of-process analyzer service.

Controlling verbosity

  • Besides emitting dependency violation issues, the tool can emit diagnostic and info messages too.
    • Info messages tell you when was the tool started and finished.
    • Diagnostic messages help you debug config problems by dumping config contents and dependency validation result cache change events.
  • When the tool is run by MSBuild you can modify the verbosity switch (/v:level) to get more or less details in the output.
    • The verbosity levels defined by MSBuild are the following (ordered from less verbose to most verbose): q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]
    • Set it to detailed or higher to see NsDepCop diagnostic messages.
    • Set it to normal or higher to see NsDepCop info messages.

Advanced settings:

  • You can also modify the importance level of NsDepCop info messages by setting the InfoImportance attribute in config.nsdepcop to Low, Normal or High.
  • This is useful if you want to keep the MSBuild verbosity level at a certain value for some reason (e.g.: because of other build steps you always want to keep verbosity at minimal level), and you want to control whether NsDepCop info messages are visible at that certain MSBuild verbosity level or not.
  • The following table shows which InfoImportance levels are shown at certain MSBuild verbosity levels.
MSBuild verbosity levelLow InfoImportanceNormal InfoImportanceHigh InfoImportance
q[uiet]---
m[inimal]--yes
n[ormal]-yesyes
d[etailed]yesyesyes
diag[nostic]yesyesyes

E.g.: if you want NsDepCop info messages to show up at minimal MSBuild verbosity then set InfoImportance to High.

Disabling to tool with MSBuild property

To disable the tool in MSBuild, set the DisableNsDepCop property to true.

<PropertyGroup>
  <DisableNsDepCop>true</DisableNsDepCop>
</PropertyGroup>

Overriding the disabled state with MSBuild property

To force executing the tool in MSBuild, set the ForceNsDepCop property to true.

msbuild MySolution.sln -t:NsDepCop_Analyze -p:ForceNsDepCop=true

Running NsDepCop only as an explicit command

If NsDepCop slows down the build too much then you can disable it as part of the build and run it explicitly before checking in.

  • Disable NsDepCop in every build by creating a file called Directory.Build.Props in your source root directory with the following content:
<Project>
 <PropertyGroup>
   <DisableNsDepCop>true</DisableNsDepCop>
 </PropertyGroup>
</Project>
  • Create a cmd file that runs only NsDepCop. Run it before every check-in.

msbuild MySolution.sln -t:NsDepCop_Analyze -p:ForceNsDepCop=true

NsDepCop ServiceHost

NsDepCop NuGet package v1.7.1 have introduced the NsDepCop ServiceHost to improve build performance.

  • It runs in the background as a standalone process, communicates via named pipes and serves requests coming from NsDepCopTask instances running inside MSBuild processes.
  • It is started automatically when needed by an NsDepCopTask and quits automatically when the MSBuild process that started it exits.
  • By running continuously it avoids the repeated startup times which is significant.

You can control the lifetime of NsDepCop ServiceHost by controlling the lifetime of the MSBuild processes by modifying the MSBUILDDISABLENODEREUSE environment variable.

  • If you set it to 1 then new MSBuild processes are started for each build and they exit when the build finishes. So do NsDepCop ServiceHost.
  • If you set it to 0 then MSBuild processes are kept alive until the Visual Studio instance that started them exits. This option gives the best build (and NsDepCop) performance.