Zügel for C#

With our newest 26.5.1 release of the Zügel MCP server we added C# as a third supported language. This article only explains the C# setup and C# specific features. If you want to learn what Zügel is and what it can do for you, please read our introduction article.

C# Setup

As always you create an .mcp.json file that tells your agent how to start Zügel, start the agent in the root directory of your solution, and ask it to call generate_config followed by reload_all. After that the agent will analyze your project and create the initial baselines.

Two things are different for C#. You need the .NET 10 SDK — the SDK, not just the runtime, because Zügel opens your solution through MSBuild and only an SDK ships MSBuild. And Zügel needs a .sln: if your repository has several, one is picked, written into the configuration and the others are named, so pointing it somewhere else is an edit rather than a guess. Nothing is downloaded — the Roslyn based parser we also use in Sonargraph ships inside the Zügel jar — and if the solution’s NuGet packages have never been restored, the first scan restores them for you.

This is how the generated zugel.json configuration file looks like for typical C# project:

{
"language": "csharp",
"project": {
"solution": "src/MyApp.sln",
"modules": [
{ "name": "MyApp", "project": "MyApp(net10.0)",
"sourceRoots": ["src/MyApp"],
"generatedSourceRoots": ["src/MyApp/obj/Debug/net10.0"] }
],
"generatedPatterns": ["**/*.designer.cs", "**/*.generated.cs", "**/*.g.cs"]
},
"arcFiles": ["architecture/MyApp.arc"]
}

A project targeting several frameworks is one project per framework as far as Roslyn is concerned, so exactly one of them is analyzed. The choice is written to project and you can change it there — componentIds never mention a framework, so adding one to a .csproj cannot invalidate a baseline. Projects declaring <IsTestProject>true</IsTestProject> are left out of the model altogether. generatedSourceRoots points into obj, where MSBuild puts generated sources; declaring it is what keeps Debug out of your componentIds.

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 forYou get
MyApp.Services.OrderServicethe file declaring that class
MyApp.Services.OrderService.Optionsthe file declaring the outer class
partial classevery 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. CSharpIsClassCSharpIsInterface 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.

Leave a Reply

Your email address will not be published. Required fields are marked *