TSpec.Architecture.CSharp 1.4.1

Prefix Reserved
dotnet add package TSpec.Architecture.CSharp --version 1.4.1
                    
NuGet\Install-Package TSpec.Architecture.CSharp -Version 1.4.1
                    
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="TSpec.Architecture.CSharp" Version="1.4.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TSpec.Architecture.CSharp" Version="1.4.1" />
                    
Directory.Packages.props
<PackageReference Include="TSpec.Architecture.CSharp" />
                    
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 TSpec.Architecture.CSharp --version 1.4.1
                    
#r "nuget: TSpec.Architecture.CSharp, 1.4.1"
                    
#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 TSpec.Architecture.CSharp@1.4.1
                    
#: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=TSpec.Architecture.CSharp&version=1.4.1
                    
Install as a Cake Addin
#tool nuget:?package=TSpec.Architecture.CSharp&version=1.4.1
                    
Install as a Cake Tool

TSpec.Architecture.CSharp

Architecture rules on the code inside the projects of a TSpec build: their source files and folders, the types those declare and their Roslyn syntax. The projects and their references are TSpec's own, in its README's Architecture section.

Install the package in your architecture spec project:

dotnet add package TSpec.Architecture.CSharp

A condensed reference for coding agents ships with the package, beside TSpec's: ~/.nuget/packages/tspec.architecture.csharp/<version>/agent-reference.md.

With using TSpec.Architecture.CSharp;, a project's Files are those it compiles from its own folder, leaving out what the build generates under obj/ and files linked in from elsewhere. A file has its Path in the project, written with /, its Lines and its Raw text, and a failure names it by project and path: Data.Auth/Stores/UserStore.cs. A file's Folder, Name and Extension are parts of its Path: for Stores/Cache/UserStore.cs, Stores, the folder directly in the project's, empty at the project's root; UserStore; and cs. Its Types are those declared at its top level, each with its Name (Repository), its FullName (Data.Repository<T>), Namespace, Kind, IsPublic, the Files it is declared in, several for a partial type, and its Nested types, declared inside it. A nested type's IsPublic is in effect: false inside a type that is not public. A failure names a type by its full name. A project's Types are those of all its files, a partial type once. A project's or a file's AllTypes are its types and those nested in them however deep, so a rule on every public type reads them, not Types. A rule of one public type per file, adopted with today's breaches listed in Allowed:

[Fact]
public void Only_the_listed_files_declare_more_than_one_public_type()
    => Source.Projects.SelectMany(project => project.Files)
        .Select(file => (file.ToString(), Public: file.Types.Count(type => type.IsPublic)))
        .Where(file => file.Public > 1)
        .Are(Allowed);

A file's Root is its Roslyn syntax as the build parsed it, so code under an #if the build left out is not in it, and its Model is the compiler's view of that syntax: what a name refers to, the type of an expression. A type the spec project does not run with, or one a source generator wrote, is unresolved there. SpanOf(node) is where a node or token starts and ends, its lines and columns counted from 1:

[Fact]
public void No_arrow_starts_a_line()
    => Source.Projects.SelectMany(project => project.Files)
        .SelectMany(file => file.Root.DescendantTokens()
            .Where(token => token.IsKind(SyntaxKind.EqualsGreaterThanToken))
            .Where(arrow => file.SpanOf(arrow).Start.Line > file.SpanOf(arrow.GetPreviousToken()).End.Line)
            .Select(arrow => $"{file}:{file.SpanOf(arrow).Start.Line}"))
        .Is().Empty();

A project's or a file's Nodes<T>() are its syntax nodes of one kind, in the order they are written: a directive such as #pragma among them, nothing inside a documentation comment. Each has its File, its Syntax and its Span, whose Lines are the lines it covers, and a failure names it by file and line: Data.Auth/Stores/UserStore.cs:12.

[Fact]
public void No_query_is_written_in_query_syntax()
    => Source.Projects["Core"].Nodes<QueryExpressionSyntax>().Is().Empty();

A file's Members are those it declares, a nested type's and local functions too, and a project's are those of all its files. A record's positional parameters are its constructor's and its properties. A member's Accessibility is the one it has in effect, no more than the types around it allow: a public member of an internal class is internal. Its Type and Parameters are syntax, as written, and a failure names it by file, line and name: Data.Auth/Stores/UserStore.cs:12 Find. A member IsOriginal when it sets its own name and signature: it neither implements an interface's member, explicitly or not, nor overrides a base type's. A naming rule reads only those:

[Fact]
public void No_method_name_ends_in_Async()
    => Source.Projects.SelectMany(project => project.Members)
        .Where(member => member.Kind == MemberKind.Method && member.IsOriginal)
        .Has().None(method => method.Name.EndsWith("Async"));

A member's Size measures its size, computed from its line count, indentation and complexity, and a file's TopLevelSize measures its top-level statements the same way:

[Fact]
public void No_member_is_over_size_240()
    => Source.Projects.SelectMany(project => project.Members).Has().None(member => member.Size > 240);

A member Gives a type when the type it returns or holds is that type, or has it written inside: as a type argument, Task<T>'s too, a tuple's element or an array's. It Takes one when a parameter's type does. A named type's own members are not inside it, so a Booking with a Status gives no enum, and a type is not one it only implements: List<int> gives no IEnumerable<>. Each takes a kind, a type whatever its type arguments, or a condition, and without one lists the types, each once:

[Fact]
public void No_contract_member_gives_an_enum_or_takes_a_list()
    => Source.Projects["Contract"].Members
        .Has().None(member => member.Gives(TypeKind.Enum) || member.Takes(typeof(List<>)));

A file's Uses are the build's types its code uses: those it names, and those whose members it names, such as an extension method's class or a constant's type; a nested type is one of its own. A type Exposes the build's types its signature names where other assemblies see it: its base type, interfaces and constraints, and the types of its public and protected members, itself included. These, Gives, Takes and DependsOn compile the project with Roslyn, once in a run, and a use by reflection is not one the compiler sees.

[Fact]
public void The_domain_uses_nothing_of_persistence()
    => Source.Projects["Core"].Files.Where(file => file.Path.StartsWith("Domain/"))
        .Has().None(file => file.Uses.Any(type => type.Namespace.EndsWith(".Persistence")));

A file DependsOn() the projects and packages whose types its code uses, named as the project rules name them: Contract, P:Dapper. Its own project and the framework are none, and a project it reaches only through another counts, unlike in its project's Dependencies. DependsOn(name) asks for one, and a name ending in * matches as in Within:

[Fact]
public void Only_the_connection_calls_Dapper()
    => Source.Projects["Data"].Files.Where(file => file.Path != "Store/Connection.cs")
        .Has().None(file => file.DependsOn("P:Dapper"));

A project's Folders are the folders directly in its own that hold its files, each with the others it uses: one of its files uses a type a file of the other declares. A file at the project's root is in no folder. They are a TSpec DependencyGraph, so Within takes rules on them as on the projects, and the generated specification draws what the rules allow:

[Fact]
public void A_subdomain_uses_only_those_below_it()
    => Source.Projects["Application"].Folders.Is().Within(folder => folder switch
    {
        "Generation" or "Assistant" => ["Chat"],
        "Workbench" => ["Generation", "Chat"],
        _ => [],
    });

Without rules, Folders.Is().Acyclic() keeps a project's folders one way.

A file is the same object every time, so an analysis TSpec doesn't have can be added to it as an extension member that keeps what it computes per file in a ConditionalWeakTable<SourceFile, T>.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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
1.4.1 155 10/25/2026
1.4.0 99 10/25/2026
1.3.0 106 10/24/2026
1.2.0 157 10/18/2026
1.1.0 94 10/18/2026
1.0.0 103 10/16/2026

FIXED
* Size: a string or a comment that spans lines no longer inflates the size of the code around it