With the 26.5.1 release of the Zügel MCP server we added C# as the third supported language. This article only explains the C#-specific features. If you want to learn what Zügel is and what it can do for you, please read our introduction article. For the initial setup please consult this article.
Rules address directories, not namespaces
This is the one thing that surprises C# developers, so it is worth saying plainly. A component is a source file and its package is the folder it sits in, exactly as in Java. C# does not require namespaces to follow folders, and where yours do not, the rules follow the folders:
include "MyApp/Services/**" // the folder MyApp/Services
include "MyApp.Services.**" // a namespace, and it matches nothing
Write forward slashes, on every platform. A componentId never contains a backslash, even when Zügel runs on Windows and your solution sits on D:\src — a rule is matched against that identifier, not against a path your shell would understand. So MyApp/Services/** is correct on Windows too, and MyApp\Services\** matches nothing at all. (Paths in zugel.json are more forgiving: Zügel writes them with forward slashes and reads either separator back happily.)
One bonus falls out of this: suggest_relocations fixes package cycles by moving files, and in C# acting on that advice costs nothing but a git mv — the namespace travels with the file.
Identifying things
A C# componentId is module/path/to/file — the source file’s location under its source root, with the extension stripped. External components also carry the assembly, because in .NET that is the unit of external identity and a namespace does not imply one: NHibernate’s Antlr.Runtime types come out of Antlr3.Runtime.dll.
MyApp/Services/OrderService
External/System.Collections/System/Collections/Generic/List<T>
Generic parameters are part of the name for external components only, because arity is part of a .NET type’s identity: Task and Task<TResult> are different types and get different components. A nested type has no component of its own — a dependency on HashSet<T>.Enumerator lands on HashSet<T>. locate_fqn resolves a type name to its component:
| You ask for | You get |
|---|---|
MyApp.Services.OrderService | the file declaring that class |
MyApp.Services.OrderService.Options | the file declaring the outer class |
a partial class | every file it is declared in |
The third row has no Java counterpart.
C# Attribute Retrievers
These let an .arc pattern match a component by what its type is rather than by where it lives. They are the same six Sonargraph offers, with the same semantics, so a rule file means the same thing in both tools.
CSharpTypeOf matches any direct or indirect supertype, base classes and interfaces alike, and is usually the one you want. CSharpExtendsClass narrows it to base classes, CSharpImplementsInterface to interfaces — including ones reached through a base class. CSharpIsClass, CSharpIsInterface and CSharpIsEnum match the type’s own name when it is of that kind.
include "CSharpImplementsInterface: **.IRepository"
include "CSharpTypeOf: Microsoft.AspNetCore.Mvc.ControllerBase"
They match dotted type names, so a single * stops at a dot. Each answers about the type named after the file, so a helper enum declared beside OrderService in OrderService.cs does not get to answer for that component.
Known limits
The hierarchy walk stops at your project. A supertype from another assembly counts by name, so CSharpTypeOf: System.Exception works, but what that type derives from is invisible — we do not parse the assemblies you reference. A class deriving from something that itself derives from ControllerBase is only found while the type in between is one of yours.
An attribute class is not a CSharpIsClass. Attributes have their own kind, in Sonargraph as well, so match them with CSharpTypeOf: System.Attribute.
Code that does not compile shows up as missing dependencies rather than as errors. Roslyn still produces a syntax tree, so what you see is references resolving to nothing. If a scan looks suspiciously sparse, check that the solution builds and its packages are restored.
Old target frameworks need their targeting packs. A net472 project is parsed anywhere — files, types and internal dependencies are all there — but without the matching .NET Framework packs installed, typically on a Mac, its references into the framework do not resolve. A model without those externals is still a great deal better than no model.