Synopsis:
This page covers CSharp_TextFinder_Structure.md, which fixes the components,
the dependency direction, the build, and the directory layout.
-
Three class libraries and one console project, matching the C++ and Rust divisions
component for component. Only the spelling changes - PascalCase here.
-
Each library is its own project rather than a folder of one assembly, so a call in
the wrong direction fails to build rather than passing review.
-
Types inside a component carry their own names rather than the component's, since a
class sharing its namespace's name makes every
using ambiguous to read.
The document declares one interface and then says plainly what binding to it does and
does not buy.
- An
IOutput interface bound through a constrained generic parameter, structurally parallel to the C++ and Rust seams.
- But a C# generic over a reference type shares one compiled body, so the constrained call is not devirtualized - it buys the compile-time bound and a substitutable test recorder, not a direct call.
- Recording that is the point: an implementer carrying the C++ design across unexamined would believe the seam costs nothing at run time.
One solution, nine projects, and no third-party package anywhere.
- The regex engine ships with the framework, so the permission to take an ecosystem package goes unused.
- That is the sharpest build difference among the three - Rust declares one crate, C++ gates
import std; behind an undocumented token, and C# declares nothing.
Nullable is enabled everywhere, because the specifications fix each failure as a returned value or a thrown exception and never as a null.
1. Three Libraries and One Binary
CSharp_TextFinder_Structure.md fixes the components, the dependency direction,
the build, and the directory layout. It is one of the two file patterns Constitution.md
rule 1 names as authoritative for code, which is what lets it declare an interface rather
than only describe a layout.
| Component |
Kind |
Responsibility |
CSharp_TextFinder_Cmdline |
class library |
Parses an argument array into ProgramCommands. Opens no stream, touches no filesystem |
CSharp_TextFinder_Dirnav |
class library |
Walks, reads, matches, formats. The only component that touches file contents. Constructs the Regex once per run |
CSharp_TextFinder_Output |
class library |
Writes each string to stdout as one line. Absorbs every write failure |
CSharp_TextFinder_Entry |
console project |
Produces the executable CSharp_TextFinder. Owns the skip list and wires the three together |
Each library's project name, assembly name, and root namespace match its component name.
The binary is the one exception and it is deliberate: the project is
CSharp_TextFinder_Entry and an AssemblyName of
CSharp_TextFinder gives the executable that name. Without that property Cargo's
and CMake's equivalent would have been unnecessary, but the .NET SDK names the output after
the project, and Spec_TextFinder.md §5.1 fixes the help text's synopsis around
CSharp_TextFinder.
The types inside a component carry their own names rather than the component's. The sink is
StdoutSink in namespace CSharp_TextFinder_Output, where the
C++ implementation names
its sink class after the module that exports it. A class sharing its namespace's name makes
every using of that namespace ambiguous to read and forces the compiler to
disambiguate in ways a reader must then follow.
Identifiers follow C# convention, so this implementation spells in PascalCase what C++
spells in camelCase and Rust in snake_case: UsageLine for
usageLine and usage_line, LineNumbers for
lineNumbers and line_numbers. The switch letters, the emitted
text, and the exit codes come from Spec_TextFinder.md and are identical in all three.
2. Dependency Direction
The binary references all three libraries. The libraries themselves form a chain rather than
a star.
CSharp_TextFinder_Cmdline
^
| references (ProgramCommands)
|
CSharp_TextFinder_Dirnav
^
| references (the IOutput interface)
|
CSharp_TextFinder_Output
^
| references (all three)
|
CSharp_TextFinder_Entry ---> CSharp_TextFinder.exe
CSharp_TextFinder_Dirnav references CSharp_TextFinder_Cmdline for
ProgramCommands, and CSharp_TextFinder_Output references
CSharp_TextFinder_Dirnav for the IOutput interface it implements.
Nothing references CSharp_TextFinder_Output but the binary, which supplies it
as the generic argument that binds Dirnav to a concrete sink.
Reading the four ProjectReference sets in order gives that chain back: Cmdline
declares none, Dirnav declares Cmdline, Output declares Dirnav, and the binary declares all
three. Each library is a separate project rather than a folder of one assembly, so a call in
the wrong direction fails to build rather than passing review.
The direction is what keeps CSharp_TextFinder_Dirnav testable without a real
sink. Its unit suite supplies its own Recorder, a class holding one
List<string> that implements IOutput, which satisfies the
same constraint the real sink does and so shows that the generic parameter binds to any
implementation of the interface.
3. The IOutput Interface and the Generic Parameter
The structure document declares one interface, and it is the seam between traversal and the
sink:
public interface IOutput
{
void Output(string text);
}
Dirnav<TOutput> binds to a concrete implementation through a generic type
parameter constrained where TOutput : IOutput. The parameter is
string rather than a span or a builder, because the sink neither stores what it
is handed nor modifies it, and string is what the framework's writers take.
What the generic parameter buys is stated in the structure document rather than assumed, and
the answer differs from both siblings. A C++ template instantiation and a Rust
monomorphization each produce a body per type argument, so the call through the constraint
is direct. A C# generic instantiated over a reference type shares one compiled body, so the
constrained call is not devirtualized. The parameter buys the compile-time bound and lets a
test substitute its own recorder; it does not buy a direct call.
Recording that is the point. An implementer who carried the C++ design across unexamined
would believe the seam costs nothing at run time, and on a reference type it costs an
interface dispatch per emitted line. The alternative the document names - passing an
IOutput reference and dropping the type parameter - would cost the same
dispatch and read more simply, and it is not adopted here, which leaves the three
implementations structurally parallel.
4. The SDK, the Solution, and No Package Reference
- C# 12, target framework
net8.0. The structure document
holds the language version and the framework once, and each component's Build section
reads "Per CSharp_TextFinder_Structure.md" and names only its own project.
- One solution at the root of the C# folder with four projects for the
components and five more for the suites and the demonstration.
Nullable enabled in every project. The specifications
fix each failure as a returned value or a thrown exception the caller handles, never as
a null result, and the compiler is what holds that.
- Toolchain minimum: .NET SDK 8.0.100. No preview language feature is
used.
No third-party package appears anywhere in the solution.
System.Text.RegularExpressions.Regex, which Spec_TextFinder.md §6.1
assigns to C#, ships with the framework, so §6's permission to take a package from the
language's ecosystem for regex access goes unused. That is the sharpest build difference
among the three implementations: Rust declares the regex crate in one manifest,
C++ gates import std; behind an undocumented CMake opt-in token whose value is
a version-specific UUID, and C# declares nothing.
Building is two lines from the C# project directory.
dotnet build CSharp_TextFinder.sln
CSharp_Spec_driven_TextFinder_Entry\bin\Debug\net8.0\CSharp_TextFinder.exe -H true
Nothing in this build is experimental and nothing about it is version-fragile. The
C++ build is the other
case, and the contrast is worth one sentence because both implement one specification: its
opt-in line will need updating when CMake withdraws the token, and this one has no such
line.
5. Where the Suites Live, and What That Costs
Spec_TextFinder.md §6.2 asks for a unit suite per library component, "each living
beside the code it tests". Each component folder therefore holds its
Spec_*.md, its project file, its sources under src/, and its
unit-test project under test/. The integration suite and the demonstration
drive the assembled executable, so both sit at the root of the C# folder, as do the three
runners.
That layout costs one line per library project, and the structure document says so rather
than leaving it to be discovered:
<Compile Remove="test/**" />
The .NET SDK compiles every .cs file beneath a project's own directory. A
library project whose folder also holds a test project would compile that suite into the
shipped assembly, along with whatever the suite's own obj/ holds. Neither
sibling faces this: Rust declares its suite with #[cfg(test)] mod unit_tests;
and the compiler omits it from a normal build, and C++ declares each source by name in a
target_sources call.
The alternative was to move the suites out of the component folders, which would have cost
nothing in build files and given up what §6.2 asked for. One line per library is the
cheaper price.
§6.2 also bars a third-party test framework, and that rules out
dotnet test, which requires one. Each suite is therefore a console project
whose Main runs its checks and returns the number that failed, and the runners
invoke those executables directly. The
Testing page covers
what that arrangement gains and gives up.
6. Source
The structure document in full, then the four component project files. The document is 52
lines and fixes everything above.
CSharp_TextFinder_Structure.md
# CSharp_TextFinder — Project Structure
The CSharp_TextFinder project comprises three libraries and one binary, arranged as a single solution. The binary references all three. The libraries themselves form a chain: `CSharp_TextFinder_Dirnav` references `CSharp_TextFinder_Cmdline` for the program-command type, and `CSharp_TextFinder_Output` references `CSharp_TextFinder_Dirnav` for the `IOutput` interface. Nothing references `CSharp_TextFinder_Output` but the binary.
## Libraries
- **CSharp_TextFinder_Cmdline** — parses the command line into a `ProgramCommands` object whose properties control the behavior of `CSharp_TextFinder_Dirnav`. C# has no free functions, so the parse entry, the usage line, the help text, and the option listing are static methods of one static class, `CommandLine`. Specified in [Spec_CSharp_TextFinder_Cmdline.md](CSharp_Spec_driven_Cmdline/Spec_CSharp_TextFinder_Cmdline.md).
- **CSharp_TextFinder_Dirnav** — directory navigation. Reads file contents, runs regex matching, and formats each matching file into the block Spec_TextFinder.md §3.4 fixes before emitting its lines. Constructs the `Regex` once per run, not once per file. Defines the interface:
```csharp
public interface IOutput
{
void Output(string text);
}
```
`Dirnav<TOutput>` binds to a concrete implementation through a generic type parameter constrained `where TOutput : IOutput`. Specified in [Spec_CSharp_TextFinder_Dirnav.md](CSharp_Spec_driven_Dirnav/Spec_CSharp_TextFinder_Dirnav.md).
- **CSharp_TextFinder_Output** — implements `IOutput.Output` according to its specification, [Spec_CSharp_TextFinder_Output.md](CSharp_Spec_driven_Output/Spec_CSharp_TextFinder_Output.md). Handles output errors internally.
Each library's project name, assembly name, and root namespace match its component name above. The types inside carry their own names rather than the component's: the sink is `StdoutSink` in namespace `CSharp_TextFinder_Output`, because a class sharing its namespace's name makes every `using` of that namespace ambiguous to read and forces the compiler to disambiguate in ways a reader must follow.
## Binary
- **CSharp_TextFinder_Entry** — binary project name; produces the executable `CSharp_TextFinder`. References the three libraries above. Specified in [Spec_CSharp_TextFinder_Entry.md](CSharp_Spec_driven_TextFinder_Entry/Spec_CSharp_TextFinder_Entry.md).
- Owns the skip list and passes it to `CSharp_TextFinder_Dirnav` for use during traversal.
- On execution, `static int Main(string[] args)` parses the command line into a `ProgramCommands` object using `CSharp_TextFinder_Cmdline`.
- A `StdoutSink` is created and bound to a `Dirnav<StdoutSink>` through its generic parameter.
- The `Dirnav` value is started at each of the specified (possibly default) root paths in turn and performs a DFS for regex matches on files in each directory tree.
- Owns the process's one output object, which in turn owns the only writer over standard output. Every write to standard output, the binary's own help text and option listing included, passes through that object, so nothing else can interleave with the search output or reorder it.
- Holds that object in a `using` declaration, so it is disposed on every path out of `Main`.
## Build
- Language: C# 12. Target framework: `net8.0`. Build system: the .NET SDK, driven by `dotnet build`. These apply to every project below, and each component's `Spec_*.md` names only its own project.
- One solution at the root of this folder with four projects, one per component. Each library is a separate project rather than a folder of one assembly, so the dependency chain above is enforced by the build rather than by convention.
- Each component has its own folder holding its `Spec_*.md`, its project file, its sources under `src/`, and — for the three libraries — its unit-test project under `test/`, which Spec_TextFinder.md §6.2 asks to live beside the code it tests. The integration suite and the demonstration drive the assembled executable, so both sit at the root of this folder rather than in a component folder, as do the three runners.
- The .NET SDK compiles every `.cs` file beneath a project's own directory, so a library project whose folder also holds a test project would compile that suite into the shipped assembly. Each library project therefore carries one `<Compile Remove="test/**" />` item. That line is the cost of putting a suite beside its component, and it is cheaper than the alternative of moving the suites away from the code they test.
- No third-party test framework, per §6.2 and the dependency rule of §6. `dotnet test` requires one, so each suite is a console project whose `Main` runs its checks and returns the number that failed, and the runners invoke those executables rather than `dotnet test`.
- Dependencies: none beyond the base class library. `System.Text.RegularExpressions.Regex`, which Spec_TextFinder.md §6.1 assigns to C#, ships with the framework, so this implementation declares no package reference at all.
- `Nullable` is enabled in every project. The specifications fix each failure as a returned value or a thrown exception the caller handles, never as a null result, and the compiler is what holds that.
- Toolchain minimum: .NET SDK 8.0.100 or later. No preview language feature is used.
## Notes
- This forms a data pipeline architecture that emits an output immediately following evaluation of a regex match.
- Identifiers follow C# convention, so this implementation spells in PascalCase what the C++ implementation spells in camelCase and the Rust implementation in snake_case: `UsageLine` for `usageLine` and `usage_line`, `LineNumbers` for `lineNumbers` and `line_numbers`. The switch letters, the emitted text, and the exit codes are fixed by Spec_TextFinder.md and are identical in all three.
- `ProgramCommands` is a class whose property initializers carry the defaults of Spec_TextFinder.md §5, so a newly constructed instance equals the result of parsing an empty command line.
- The `IOutput` interface with a generic parameter is the current design choice. Passing an `IOutput` reference directly is a viable alternative that trades the type parameter for a shorter signature; it is not adopted here. One difference from the siblings is worth recording rather than assuming: a C# generic instantiated over a reference type shares one compiled body, so the constrained call is not devirtualized the way a C++ template instantiation or a Rust monomorphization is. The parameter buys the compile-time bound and lets a test substitute its own recorder; it does not buy a direct call.
- Spec_TextFinder.md §3.4 obliges an implementation to stop its runtime translating the LF terminator to CRLF. `Console.WriteLine` terminates with `Environment.NewLine`, which is CRLF on Windows, so this implementation does not use it. `CSharp_TextFinder_Output` owns a `StreamWriter` over `Console.OpenStandardOutput()` with `NewLine` set to `"\n"`, and [Spec_CSharp_TextFinder_Output.md](CSharp_Spec_driven_Output/Spec_CSharp_TextFinder_Output.md) records what follows from that.
- C# has no destructor that runs at scope exit, so the sink implements `IDisposable` and the binary disposes it with a `using` declaration. A finalizer is not relied on for the flush: the runtime is free never to run one.
- `Main`'s `string[] args` holds the arguments alone, where C++'s `argv[0]` and Rust's first `args_os()` element hold the executable name. The bare command line of Spec_TextFinder.md §3.1 is therefore an empty array here rather than a one-element one, and the parser scans from index 0. Each component spec states its own offset rather than inheriting a sibling's.
- A `string` is UTF-16 and the runtime decodes the command line before `Main` sees it, so the undecodable-argument case the Rust implementation defines has no counterpart here and needs no exit code of its own.
- `Dirnav` holds references to the sink, the skip list, and the parsed commands, and the garbage collector keeps them alive as long as it does. The lifetime rule the C++ implementation states in prose and the Rust implementation enforces with a borrow checker therefore has nothing to enforce here.
- The skip list is a static field of the entry project, reachable without a lock and without an interior-mutability type. The `thread_local!` and `RefCell` pair the Rust implementation needs has no counterpart: the list is built before traversal begins and traversal runs on one thread.
CSharp_Spec_driven_Cmdline/CSharp_TextFinder_Cmdline.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>CSharp_TextFinder_Cmdline</AssemblyName>
<RootNamespace>CSharp_TextFinder_Cmdline</RootNamespace>
</PropertyGroup>
<!-- Structure doc, Build: the SDK compiles every .cs beneath this project, so the
unit-test project sitting beside the code it tests must be removed by hand. -->
<ItemGroup>
<Compile Remove="test/**" />
</ItemGroup>
</Project>
CSharp_Spec_driven_Dirnav/CSharp_TextFinder_Dirnav.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>CSharp_TextFinder_Dirnav</AssemblyName>
<RootNamespace>CSharp_TextFinder_Dirnav</RootNamespace>
</PropertyGroup>
<!-- Spec §9: Cmdline for ProgramCommands. System.Text.RegularExpressions ships with
the framework, so §6.1's engine costs no package reference. -->
<ItemGroup>
<ProjectReference Include="../CSharp_Spec_driven_Cmdline/CSharp_TextFinder_Cmdline.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="test/**" />
</ItemGroup>
</Project>
CSharp_Spec_driven_Output/CSharp_TextFinder_Output.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>CSharp_TextFinder_Output</AssemblyName>
<RootNamespace>CSharp_TextFinder_Output</RootNamespace>
</PropertyGroup>
<!-- Spec §8: Dirnav for the IOutput interface, and nothing else. -->
<ItemGroup>
<ProjectReference Include="../CSharp_Spec_driven_Dirnav/CSharp_TextFinder_Dirnav.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="test/**" />
</ItemGroup>
</Project>
CSharp_Spec_driven_TextFinder_Entry/CSharp_TextFinder_Entry.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Spec §7: the project is CSharp_TextFinder_Entry, the executable CSharp_TextFinder. -->
<AssemblyName>CSharp_TextFinder</AssemblyName>
<RootNamespace>CSharp_TextFinder_Entry</RootNamespace>
<!-- Spec §7: no comparison here is culture-sensitive, so one added later fails visibly. -->
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../CSharp_Spec_driven_Cmdline/CSharp_TextFinder_Cmdline.csproj" />
<ProjectReference Include="../CSharp_Spec_driven_Dirnav/CSharp_TextFinder_Dirnav.csproj" />
<ProjectReference Include="../CSharp_Spec_driven_Output/CSharp_TextFinder_Output.csproj" />
</ItemGroup>
</Project>
Four project files and not one of them names a package. The chain of Section 2 is the four
ProjectReference sets read in order, and the two properties that carry a
decision rather than a default are the binary's AssemblyName and its
InvariantGlobalization.
7. Prompt Records
This page carries none. Page_Structure.md §8 assigns it
Prompts_CSharp_TextFinder_Structure.md and its Fix companion, and
neither was written: this thread produced one record covering every turn, and §8's rule
that each record has exactly one home puts it on the
Process page. The turn
that wrote the structure document, and the amendment to it that fixed the layout of
Section 5, are both summarized there.