Solution.Parser
4.0.0-alpha.17
See the version list below for details.
dotnet add package Solution.Parser --version 4.0.0-alpha.17
NuGet\Install-Package Solution.Parser -Version 4.0.0-alpha.17
<PackageReference Include="Solution.Parser" Version="4.0.0-alpha.17" />
<PackageVersion Include="Solution.Parser" Version="4.0.0-alpha.17" />
<PackageReference Include="Solution.Parser" />
paket add Solution.Parser --version 4.0.0-alpha.17
#r "nuget: Solution.Parser, 4.0.0-alpha.17"
#:package Solution.Parser@4.0.0-alpha.17
#addin nuget:?package=Solution.Parser&version=4.0.0-alpha.17&prerelease
#tool nuget:?package=Solution.Parser&version=4.0.0-alpha.17&prerelease
Solution Parser
Packages
The library is split so that a consumer only pays for the part it uses. Solution.Parser still
pulls everything, so an existing reference needs no change.
| Package | 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.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 |
meta package, pulls 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 whichever language package you reference.
project.SourceFiles.CSharpFiles() // needs Solution.Parser.CSharp
project.SourceFiles.XamlFiles() // needs Solution.Parser.Xaml
That is what keeps XAML out of a C# only consumer and the other way round.
Solution formats
Both the classic .sln and the new xml based .slnx format are supported:
// classic
var solutionFileInfo = new SolutionFileName("MySolution.sln").FindSolutionFileReverseFrom(startUpDirectory);
// slnx
var solutionFileInfo = new SolutionFileName("MySolution.slnx").FindSolutionFileReverseFrom(startUpDirectory);
// either of both, '.slnx' wins when both files exist side by side
var solutionFileInfo = SolutionFileName.WithAnySolutionFormat("MySolution").FindSolutionFileReverseFrom(startUpDirectory);
SolutionFileInfo.IsSlnx tells which format was found. Everything behind Parse() is format agnostic.
Sample
[TestClass]
public abstract class MsTestBase
{
protected static IImmutableList<CSharpSyntaxTree> TestCode { get; private set; } = ImmutableList<CSharpSyntaxTree>.Empty;
protected static IImmutableList<CSharpSyntaxTree> AllSyntaxTrees { get; private set; } = ImmutableList<CSharpSyntaxTree>.Empty;
protected static IImmutableList<CSharpSyntaxTree> ProductiveCode { get; private set; } = ImmutableList<CSharpSyntaxTree>.Empty;
protected static IImmutableList<CSharpSyntaxTree> ProductiveCodeToAnaylze { get; private set; } = ImmutableList<CSharpSyntaxTree>.Empty;
protected static SolutionFile Solution { get; private set; } = null!;
[AssemblyInitialize]
public static void Init(TestContext _)
{
var sSolutionFileInfo = new SolutionFileName("MySolution.sln").FindSolutionFileReverseFrom(new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory));
Throw.IfNull(sSolutionFileInfo);
Solution = sSolutionFileInfo.Parse();
ProductiveCode = Solution.ProductiveProjects.SelectMany(p => p.CSharpFileInfos)
.Select(c => c.Parse())
.ToImmutableList();
TestCode = Solution.UnitTestProjects.SelectMany(p => p.CSharpFileInfos)
.Select(c => c.Parse())
.ToImmutableList();
AllSyntaxTrees = ProductiveCode.Concat(TestCode).ToImmutableList();
}
}
CodeRule sample
[TestCategory("Coding-Rules")]
[TestCategory("Coding-Rules Records")]
[TestClass]
public class Records : MsTestBase // <-- MsTestBase is a base class that provides the ProductiveCode property
{
[TestMethod]
public void Record_Properties_Have_To_Be_Be_Immutable()
{
var mutableProperties = (from syntaxTree in ProductiveCode
from @record in syntaxTree.Records
from property in @record.Properties
where property.IsReadOnly.IsFalse()
select new
{
Error = $@"
Please do not use mutable properties
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
FullName: {@record.FullQualifiedName}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Property: {property.Type} {property.Name}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Should be: {property.Type} {{get; init;}}
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
"
}).ToImmutableList();
Assert.IsTrue(mutableProperties.IsEmpty(),
$"Properties must be immutable. Findings: '{mutableProperties.Count}':{Environment.NewLine}{mutableProperties.Select(error => error.Error).Flatten(Environment.NewLine)}{Environment.NewLine}");
}
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 tree in ProductiveCode
from type in tree.AllTypes()
from property in type.Properties
where property.IsReadOnly.IsFalse()
select $"{property.Location}: {type.FullQualifiedName}.{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
ProductiveCode.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).
XAML
.xaml files parse into the same shape of model, so a rule can reach across markup and code behind:
var tree = new XAMLFileInfo(@"D:\repo\src\MainWindow.xaml").Parse();
tree.FullQualifiedName // "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 throughd: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 from 5.x
| 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 |
seven packages plus the meta package; see the table at the top |
project.CSharpFileInfos |
project.SourceFiles.CSharpFiles() |
project.XAMLFileInfos |
project.SourceFiles.XamlFiles() |
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, FullQualifiedName 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 |
| Product | Versions 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. |
-
net10.0
- Extensions.Pack (>= 7.0.0)
- FileSystem.Abstraction (>= 0.1.2)
- Microsoft.Build (>= 18.4.0)
- Microsoft.CodeAnalysis.CSharp (>= 5.3.0)
- Microsoft.VisualStudio.SolutionPersistence (>= 1.0.52)
- NuGet.Configuration (>= 7.3.0)
- NuGet.Versioning (>= 7.3.0)
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 |
|---|---|---|
| 5.0.0-alpha.8 | 0 | 9/22/2026 |
| 4.0.0 | 41,619 | 4/8/2026 |
| 4.0.0-alpha.17 | 46 | 9/15/2026 |
| 4.0.0-alpha.12 | 249 | 3/24/2026 |
| 3.0.8 | 13,860 | 1/9/2026 |
| 3.0.7 | 4,303 | 11/23/2025 |
| 3.0.6 | 268 | 11/21/2025 |
| 3.0.5 | 3,740 | 10/29/2025 |
| 3.0.4 | 54,644 | 9/8/2025 |
| 3.0.3 | 1,828 | 9/7/2025 |
| 3.0.2 | 16,054 | 2/4/2025 |
| 3.0.1 | 10,768 | 1/24/2025 |
| 3.0.0 | 13,662 | 11/20/2024 |
| 2.0.18 | 110,014 | 11/20/2024 |
| 2.0.17 | 13,253 | 10/17/2024 |
| 2.0.16 | 49,279 | 9/23/2024 |
| 2.0.15 | 5,837 | 9/6/2024 |
| 2.0.13 | 16,050 | 6/12/2024 |
| 2.0.12 | 5,427 | 5/26/2024 |
| 2.0.11 | 6,360 | 5/12/2024 |
C# breaking: member lists now hold only the members a type declares itself; use AllTypes(), DescendantTypes() and AllMethods() for the recursive walk. Class, Struct, Record, Interface and Enum are now siblings below TypeDeclaration with a TypeKind. Declarations carry Location, Accessibility, XML documentation and type parameters. Delegates, indexers, operators and finalizers are parsed. C# fixes: fully qualified names under a file scoped or nested namespace, SyntaxTree returning the whole file, only the first variable of a shared field declaration, missing modifiers such as sealed, virtual, override, new and async, IsNullable on a nullable type argument, IsReadOnly decided by text search, and line splitting on the platform line ending. Namespaces: Solution.Parser.XAML became Solution.Parser.Xaml and XAMLFileInfo became XamlFileInfo; Solution.Parser.Solution became Solution.Parser.Sln and Solution.Parser.Common became Solution.Parser.Core. ProjectFile.CSharpFileInfos and ProjectFile.XAMLFileInfos became the extension methods project.CSharpFiles() and project.XamlFiles(); the project directory is now enumerated once instead of once per file type. XAML breaking: the element tree is now built from direct children, so Controls became Children and holds only what an element declares itself; use AllElements(), DescendantElements(), AllStyles(), AllBindings() and FindByName() for the recursive walk. Property elements such as Grid.RowDefinitions are properties rather than children. Window, UserControl, Page, Application and ResourceDictionary are siblings below Root with a RootKind; Style, Setter, Trigger and Template are siblings below ElementBase with an ElementKind. Models are records with init properties, LineNumber became Location, and a file that cannot be read fully reports XamlSyntaxTree.Diagnostics instead of throwing. XAML fixes: nesting was flattened and duplicated and the parse was exponential in the nesting depth, Parent was always null, Styles was always empty, DataContext was never null, a plain value containing a colon and a StringFormat containing a comma each killed the whole file, the {} escape was mistaken for markup, x:Name was indistinguishable from Name, an attached property was named RowProperty so element["Grid.Row"] never matched, x:Static returned an x:Type, RelativeSource folded its ancestor type into its mode, and element text content was dropped. Support for the new xml based slnx solution format.