Solution.Parser 6.0.2

This package has a SemVer 2.0.0 package version: 6.0.2+1.
dotnet add package Solution.Parser --version 6.0.2
                    
NuGet\Install-Package Solution.Parser -Version 6.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Solution.Parser" Version="6.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Solution.Parser" Version="6.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Solution.Parser" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Solution.Parser --version 6.0.2
                    
#r "nuget: Solution.Parser, 6.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Solution.Parser@6.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Solution.Parser&version=6.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Solution.Parser&version=6.0.2
                    
Install as a Cake Tool

Solution.Parser

Build NuGet Downloads

Quick start

A code rule is a query over the solution and an assertion that it finds nothing:

using Solution.Parser.Core;     // ShouldBeEmpty
using Solution.Parser.CSharp;   // the C# model and its queries
using Solution.Parser.Sln;      // SolutionFile

var solution = SolutionFile.Find("MySolution.sln");   // searched upwards from the running test

var findings = from type in solution.ProductionTrees.AllTypes()
               from property in type.Properties
               where !property.IsReadOnly
               select $"{property.Location}: {type.FullyQualifiedName}.{property.Name} is mutable";

findings.ShouldBeEmpty("Properties must be immutable");

A failing rule lists every finding on its own line, as path(line,column): message, which test runners and IDEs turn into a link:

Properties must be immutable. 2 findings:
  D:\repo\src\Person.cs(12,5): My.Sample.Person.Name is mutable
  D:\repo\src\Order.cs(8,5): My.Sample.Order.Total is mutable

Packages

One package ships: Solution.Parser. It contains everything, so an existing reference needs no change. The split below is how the source is organised; the assemblies all travel inside that one package and none of them is published on its own.

Assembly What it parses What it drags in
Solution.Parser.Core shared contracts, nothing on its own —
Solution.Parser.CSharp C# files into syntax trees Roslyn
Solution.Parser.CSharp.Semantic symbols: which method a call binds to, which parameter an argument fills Roslyn, CSharp and Sln
Solution.Parser.Xaml XAML markup nothing but Core
Solution.Parser.Project csproj, old and SDK style nothing but Core
Solution.Parser.Sln sln and slnx, and their projects MSBuild
Solution.Parser.Nuspec nuspec files and NuGet folders nothing but Core
Solution.Parser.AspNet controllers, routes, response types NuGet client libraries
Solution.Parser the shipped package, contains all of the above all of the above

Solution.Parser.Project deliberately knows no language: ProjectFile.SourceFiles is a plain file list, and the typed views come from the language assembly you use.

project.SourceFiles.GetCSharpFiles()   // Solution.Parser.CSharp
project.SourceFiles.GetXamlFiles()     // Solution.Parser.Xaml

Should the parts ever be published separately, that boundary is what lets a C# only consumer skip XAML and the other way round. Today it is a source boundary, not a packaging one.

Solution formats

Both the classic .sln and the new xml based .slnx format are supported:

SolutionFile.Find("MySolution.sln")    // exactly that file
SolutionFile.Find("MySolution.slnx")   // exactly that file
SolutionFile.Find("MySolution")        // either of both, '.slnx' wins when both exist side by side

SolutionFile.Find("MySolution.sln", startDirectory: new DirectoryInfo(@"D:\repo"))   // search from elsewhere

solution.FileInfo.Extension tells which format was found. Everything behind it is format agnostic. A solution whose path is already known is parsed with SolutionFile.Parse(path).

Solution, projects and code

solution.Projects                       // every project
solution.ProductionProjects             // the ones that are not test projects
solution.UnitTestProjects
solution.Project("My.Sample")           // by name; an unknown name lists the ones there are

solution.AllTrees                       // the C# code of every project, parsed once and cached
solution.ProductionTrees
solution.UnitTestTrees
project.Trees

project.Name                            // "My.Sample"
project.Path                            // D:\repo\src\My.Sample\My.Sample.csproj
project.PackageReferences               // Include and Version

A file is a plain FileInfo: solution.FileInfo, project.FileInfo, project.SourceFiles. What kind of file it is shows in the type parsing it returns, so each of them parses with a static Parse that takes a path or a FileInfo: SolutionFile.Parse, ProjectFile.Parse, CSharpSyntaxTree.Parse, XamlSyntaxTree.Parse, NuspecFile.Parse.

A base class for a rule suite

[TestClass]
public abstract class CodeRuleBase
{
    protected static SolutionFile Solution { get; private set; } = null!;

    [AssemblyInitialize]
    public static void Init(TestContext _) => Solution = SolutionFile.Find("MySolution.sln");
}

[TestClass]
public class Records : CodeRuleBase
{
    [TestMethod]
    public void Record_Properties_Have_To_Be_Immutable()
    {
        var findings = from tree in Solution.ProductionTrees
                       from record in tree.Records
                       from property in record.Properties
                       where !property.IsReadOnly
                       select $"{property.Location}: {record.FullyQualifiedName}.{property.Name} should be {{ get; init; }}";

        findings.ShouldBeEmpty("Record properties must be immutable");
    }
}

What the model gives you

Every declaration carries a Location, so a finding points at the source instead of only naming the type. Location.ToString() renders path(line,column), which test runners and IDEs turn into a link:

var findings = from type in solution.ProductionTrees.AllTypes()
               from property in type.Properties
               where !property.IsReadOnly
               select $"{property.Location}: {type.FullyQualifiedName}.{property.Name} is mutable";

// D:\repo\src\Person.cs(12,5): My.Sample.Person.Name is mutable

Beyond that, a declaration reports its Accessibility (including the default that applies when no modifier is written), its Documentation (the XML comment), its Modifiers (now complete, including sealed, virtual, override, new, async and readonly), its TypeParameters with their constraints, and its Attributes split into positional and named arguments. HasAttribute("Obsolete") matches [Obsolete], [ObsoleteAttribute] and [System.ObsoleteAttribute] alike.

Types also report Indexers, Operators, Delegates and Finalizers, and TypeKind tells a record from a record struct. tree.Diagnostics surfaces what Roslyn reported while parsing, so a rule can assert that no file has a syntax error rather than silently trusting an incomplete model.

Nesting

A member belongs to the type that declares it. type.Methods holds the methods of that type, and the methods of a nested type belong to the nested type:

tree.Types              // the types declared at file level
type.NestedTypes        // the types declared directly inside this type
type.Methods            // the methods declared directly in this type

To walk everything, ask for it:

tree.AllTypes()               // every type of the file, nested ones included
tree.AllMethods()             // every method of the file
type.DescendantTypes()        // every type declared inside this one, at any depth
type.AllMethods()             // this type and every type nested in it
solution.AllTrees.AllTypes()  // the same across a whole solution

Predicates that read the way a rule means: IsPublic(), IsStatic(), IsSealed(), IsAbstract(), IsPartial(), IsOverride(), IsVirtual(), HasAttribute(name), Implements(name), InheritsFrom(name).

Every member with a body reports the method calls in it, lambdas included, a local function on its own:

method.Invocations                         // Client.AssertPostAsync("..", "..", true)
tree.AllInvocations().Named("AssertPostAsync")   // Named works on any declaration list
call.Target                                // "Client"
call.Arguments                             // as written, in source order
call.NamedArgument("writeResponse")        // only what is written as writeResponse: true
call.ArgumentFor("writeResponse")          // by position, by name or by default; needs symbols, see below

Symbols

The syntax alone cannot tell that true in Client.AssertPostAsync("..", "..", true) is the parameter writeResponse, least of all when the method comes from a NuGet package. The compiler can. The parse mode of the solution decides which of the two you get, over the same records:

var solution = SolutionFile.Find("MySolution.sln", ParseMode.SyntaxOnly);   // syntax only, parsed once and cached
var solution = SolutionFile.Find("MySolution.sln", ParseMode.WithSymbols);  // plus a Roslyn compilation per project, built on first use

var findings = from call in solution.UnitTestTrees.AllInvocations().Named("AssertPostAsync")
               where call.Passes("writeResponse", true)
               select $"{call.Location}: writeResponse is true";

The code hangs on the solution and its projects: solution.AllTrees, solution.ProductionTrees, solution.UnitTestTrees, project.Trees, solution.CompilationDiagnostics() and project.CompilationDiagnostics(). A solution parsed without a mode gets the syntax on first use.

What a resolved call tells, in the vocabulary of the syntax model:

call.Resolution            // Resolved | Ambiguous | Unresolved
call.Method                // a Method record; for code of the solution the very record the tree holds
call.Method.Origin         // Source | Package("AspNetCore.Simple.MsTest.Sdk", "9.5.15") | Framework
call.BoundArguments        // every parameter with what is passed for it, in parameter order
call.ArgumentFor("writeResponse").Expression    // "true", by position or by name alike
call.ArgumentFor("writeResponse").IsExplicit    // false when left out and the default applies
call.ArgumentFor("writeResponse").Is(true)      // literal, const or default value alike
call.Passes("writeResponse", true)              // the same in one go, false for an unresolved call
call.Candidates            // the competing overloads of an ambiguous call
call.Roslyn                // node, semantic model, symbol and operation, for anything not covered

The compilation is built from what restore and build left on disk: package assemblies from obj/project.assets.json, the framework reference packs, project references as compilations, the generated global usings and XAML output from obj, and the source generators of frameworks and packages. The solution therefore has to be restored, which it is whenever its tests run. project.CompilationDiagnostics() shows what the compiler could not resolve.

With ParseMode.SyntaxOnly a symbol access throws instead of guessing, so the cost of a rule stays visible: nothing is compiled unless a rule asks for symbols. CodeBase.FromSource(code).WithSymbols() gives the same API for code in memory, which is handy for trying a rule out; CodeBase.FromSources(("A.cs", a), ("B.cs", b)) takes several files.

XAML

.xaml files parse into the same shape of model, so a rule can reach across markup and code behind:

var tree = XamlSyntaxTree.Parse(@"D:\repo\src\MainWindow.xaml");

tree.FullyQualifiedName          // "My.Sample.MainWindow", the x:Class
tree.Root                       // a Window, UserControl, Page, Application,
                                // ResourceDictionaryRoot or ControlRoot
tree.Diagnostics                // what could not be read, instead of a lost file

For markup that is not on disk, for example in a test:

var tree = xamlContent.ParseXaml(@"D:\repo\src\MainWindow.xaml");

Children holds what an element declares itself, and a property element such as <Grid.RowDefinitions> is a property of the Grid rather than a child of it, which is what XAML means by it:

element.Children                // the elements written directly inside this one
element.Properties              // attributes, attached properties and property elements
element.Resources               // what <X.Resources> declares
element["Grid.Row"]             // an attached property, by the name it is written with
element.Content                 // the text of <Button>Click me</Button>
element.Parent                  // the element this one is written in
element.Location                // path(line,column), clickable in a test runner

To walk everything, ask for it, the same way as on the C# side:

tree.AllElements()              // every element of the file
tree.AllStyles()                // every Style, however deep in the resource dictionaries
tree.AllTemplates()             // DataTemplate, ControlTemplate, ItemsPanelTemplate, …
tree.AllBindings()              // every Binding, nested ones included
tree.AllMarkupExtensions()      // every {…}, nested ones included
tree.FindByName("Save")         // by x:Name
tree.FindByKey("OkButton")      // by x:Key
tree.OfTypeName("Button")       // every element written as <Button>
element.Ancestors()             // up the tree, nearest first

Values keep the shape they were written in, so a rule reads the part it cares about rather than the raw string:

var binding = textBlock["Text"]?.PropertyValue as Binding;

binding.Path?.ValueText                          // "Total"
(binding.Converter as StaticResource)?.ResourceKey   // "MoneyConverter"
binding.RelativeSource?.AncestorType             // "Window"
binding.StringFormat?.ValueText                  // "{0:#,##0.00} EUR"

Binding, MultiBinding, StaticResource, DynamicResource, TemplateBinding, RelativeSource, XTypeMarkupExtension, XStaticMarkupExtension and NullExtension are all MarkupExtension, and an extension the parser has no model for keeps its name and arguments instead of being reduced to text. A value is markup only when it starts with an unescaped { followed by a name, so a pack URI, a caption with a colon and a {}{0:N2} escape are all plain text.

Style, Setter, Trigger, Template, ResourceDictionaryElement and Control are siblings below ElementBase with an ElementKind; Window, UserControl, Page, Application, ResourceDictionaryRoot and ControlRoot are siblings below Root with a RootKind.

Sample application

src/SampleApp.Wpf is a small but real WPF application: a window, a user control, a theme dictionary, view models and bindings. It is a fixture rather than a demo. Because it is a real UseWPF project, markup that WPF would reject cannot get in, so the parser tests always run against XAML that actually compiles. Nothing in the library references it.

src/SampleApp.Wpf.Test is what a consumer of the packages looks like: it references Solution.Parser.Xaml, .CSharp and .Sln, finds the application through the solution, and runs code rules over it. Two of them are worth reading as examples:

  • BindingRule - every {Binding Path=X} has to name a property that exists on the view model the view declares through d:DataContext. A typo there compiles, renders nothing and is caught by no compiler.
  • ResourceRule - every {StaticResource Key} used anywhere has to be declared somewhere, across files.

Each rule is also run against a deliberately broken view parsed from memory, because a rule that never fails proves nothing.

Migrating

Upgrading from an older major? See MIGRATION.md — it covers the mechanical renames and, more importantly, the changes that still compile but report something different. It is written so you can hand it to an AI assistant along with your rule suite.

The short version for 6.0, all of it caught by the compiler:

5.x 6.0
FullQualifiedName, NameSpace FullyQualifiedName, Namespace
declaration.SyntaxTree (a string) declaration.SourceCode
ProductiveProjects, ProductiveTrees ProductionProjects, ProductionTrees
call.Argument("x") call.ArgumentFor("x"), or call.Passes("x", value)
fileInfo.Value.FullName, package.PackageId.Value fileInfo.FullName, package.Id
project.ProjectFileInfo.Value.FullName project.Path
new SolutionFileName(x).FindSolutionFileReverseFrom(dir).Parse() SolutionFile.Find(x), with dir as optional start
new SolutionFileInfo(path).Parse(), csharpFileInfo.Parse() SolutionFile.Parse(path), CSharpSyntaxTree.Parse(path)
solution.SolutionFileInfo, project.ProjectFileInfo solution.FileInfo, project.FileInfo, a plain FileInfo
attributes.Named(x) returned the first match it returns all of them; declaration.Attribute(x) for one

And for 5.0:

Before Now
type.Methods returned nested types' methods too type.AllMethods()
tree.Classes contained nested classes tree.AllTypes(), filtered by kind
type.NestedClasses contained grandchildren type.DescendantTypes()
Record : Class : Interface all siblings below TypeDeclaration, with Kind
class.Interfaces (was a duplicate of NestedInterfaces) BaseTypes, or Implements(name)
method.SyntaxTree returned the whole file it is now the method's own source; the file is tree.SyntaxTree
new Class(...) positional object initializer, new Class { Name = ..., ... }

Classes, Records, Interfaces, Structs and Enums still exist on the tree, and NestedClasses, NestedStructs, NestedInterfaces and NestedEnums still exist on a type; they are now filtered views over Types and NestedTypes. Method.MethodValue and Method.MethodBody are kept as the previous names for SyntaxTree and Body.

Packaging and projects

Before Now
one Solution.Parser package unchanged: still one package, now holding seven assemblies
project.CSharpFileInfos project.SourceFiles.GetCSharpFiles()
project.XAMLFileInfos project.SourceFiles.GetXamlFiles()
namespace Solution.Parser.Common Solution.Parser.Core
namespace Solution.Parser.Solution Solution.Parser.Sln
namespace Solution.Parser.XAML Solution.Parser.Xaml
XAMLFileInfo XamlFileInfo

XAML

Before Now
element.Controls held every descendant, each duplicated element.Children, or tree.AllElements()
element.Parent was always null it points at the element this one is written in
element.Styles was always empty tree.AllStyles(), and <Style> is now a Style
element.DataTemplates tree.AllTemplates() / tree.AllDataTemplates()
element.DataContext was set on every element null unless the element sets one
element.LineNumber element.Location, with line, column and path
<Grid.RowDefinitions> was a control named Grid.RowDefinitions a PropertyElement in Properties
attached property named RowProperty Name is Row, FullyQualifiedName is Grid.Row, DependencyPropertyName is RowProperty
x:Name and Name were both called Name Property.Prefix and Property.IsXamlDirective tell them apart
Window : UserControl, DynamicResource : StaticResource siblings, with RootKind and a shared ResourceReference
DataTemplate, ResourceDictionary, ResourceDictionaryControl Template, ResourceDictionaryRoot, ResourceDictionaryElement
new Control(...) positional object initializer, new Control { TypeName = ..., ... }
an unreadable value threw and lost the file tree.Diagnostics

License

Copyright 2022-2026 (c) Rene Peuser.

Licensed under the Apache License 2.0.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
6.0.2 178 10/13/2026
6.0.1 21 10/13/2026
6.0.0 22 10/13/2026
5.1.0-alpha.11 46 10/8/2026
5.1.0-alpha.7 47 10/8/2026
5.0.2 1,202 10/8/2026
5.0.1 2,324 9/27/2026
5.0.0 47 9/24/2026
5.0.0-alpha.8 61 9/22/2026
4.0.0 45,349 4/8/2026
4.0.0-alpha.17 70 9/15/2026
4.0.0-alpha.12 261 3/24/2026
3.0.8 13,871 1/9/2026
3.0.7 4,316 11/23/2025
3.0.6 277 11/21/2025
3.0.5 3,750 10/29/2025
3.0.4 54,665 9/8/2025
3.0.3 1,839 9/7/2025
3.0.2 16,062 2/4/2025
3.0.1 10,783 1/24/2025
Loading failed

Major release with breaking changes. Full migration guide in MIGRATION.md. New: .NET 10 support, slnx solution format, Location on all declarations (clickable path(line,column)), complete modifier support (sealed/virtual/override/async), indexers/operators/finalizers, XML documentation, TypeKind, better XAML element tree. Breaking: Namespaces renamed (XAML→Xaml, Solution→Sln, Common→Core), member lists now non-recursive (use AllTypes()/AllMethods()), type hierarchy changed (Class/Record/Struct/Interface are siblings), XAML Controls→Children, project.CSharpFileInfos→SourceFiles.CSharpFiles(). Fixes: SyntaxTree scope, shared field declarations, IsNullable/IsReadOnly/IsAsync accuracy, XAML Parent/nesting/property elements.