| name | dotnet-architecture |
| description | Project structure rules, MSBuild linting, and Clean Architecture principles for the DataNormalizer repository. Covers solution layout, project references, Directory.Build.props/Directory.Packages.props linting rules, and the relationship between runtime library, source generator, tests, and samples. |
Project Architecture
MSBuild Linting Rules
These rules ensure consistent MSBuild configuration across the solution.
RULE_A: No Hardcoded Versions in Directory.Packages.props
Package versions in Directory.Packages.props should use meaningful version values. If the project defines version variables (e.g., in Version.props), use them consistently rather than hardcoding the same version in multiple places.
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="4.12.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="$(RoslynVersion)" />
RULE_B: Version.props Import Locations
If the project uses a Version.props file, it should only be imported in Directory.Build.props or Directory.Packages.props โ never in individual .csproj files.
RULE_G: No Package Versions in .csproj
<PackageReference> elements in .csproj files must NEVER include a Version attribute when CPM is enabled. All versions belong in Directory.Packages.props.
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit" />
Repository Structure
DataNormalizer/
โโโ src/
โ โโโ DataNormalizer/ # Runtime library (net6.0;net7.0;net8.0;net9.0;net10.0)
โ โ โโโ Attributes/ # Marker attributes for source generator
โ โ โโโ Configuration/ # Fluent builder API (no-op at runtime)
โ โ โโโ Runtime/ # NormalizationContext
โ โ โโโ DataNormalizer.csproj
โ โโโ DataNormalizer.Generators/ # Roslyn source generator (netstandard2.0)
โ โโโ Analysis/ # ConfigurationParser, TypeGraphAnalyzer
โ โโโ Emitters/ # DtoEmitter, NormalizerEmitter, etc.
โ โโโ Models/ # Immutable data models for pipeline
โ โโโ Diagnostics/ # DiagnosticDescriptors (DN0001-DN0004)
โ โโโ NormalizeGenerator.cs # IIncrementalGenerator entry point
โ โโโ DataNormalizer.Generators.csproj
โโโ tests/
โ โโโ DataNormalizer.Tests/ # Runtime unit tests (NUnit 4)
โ โโโ DataNormalizer.Generators.Tests/ # Generator snapshot tests (Verify)
โ โโโ DataNormalizer.Integration.Tests/ # End-to-end tests with real generated code
โโโ samples/
โ โโโ DataNormalizer.Samples/ # Example usage (console app)
โโโ docs/
โ โโโ plans/ # Design and implementation docs
โโโ .opencode/ # AI agent skills and context
โโโ DataNormalizer.sln
โโโ Directory.Build.props # Shared build settings
โโโ Directory.Packages.props # Central Package Management
โโโ .csharpierrc.yaml # CSharpier config
โโโ .editorconfig # Editor settings
โโโ .github/workflows/ # CI/CD
Project Dependency Rules
DataNormalizer (runtime)
โโโ References: DataNormalizer.Generators (as Analyzer, not assembly)
DataNormalizer.Generators (source generator)
โโโ References: Microsoft.CodeAnalysis.CSharp, PolySharp (all PrivateAssets)
DataNormalizer.Tests
โโโ References: DataNormalizer (runtime only)
DataNormalizer.Generators.Tests
โโโ References: DataNormalizer.Generators + DataNormalizer + Microsoft.CodeAnalysis.CSharp
DataNormalizer.Integration.Tests
โโโ References: DataNormalizer (which includes the generator via Analyzer)
DataNormalizer.Samples
โโโ References: DataNormalizer (which includes the generator via Analyzer)
Critical: Generator as Analyzer Reference
The runtime library references the generator project as an analyzer, not a regular assembly reference. This is what makes the generator run at compile time for consumers:
<ItemGroup>
<ProjectReference Include="..\DataNormalizer.Generators\DataNormalizer.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
The generator DLL is also packed into the NuGet package under analyzers/dotnet/cs/:
<ItemGroup>
<None Include="..\DataNormalizer.Generators\bin\$(Configuration)\netstandard2.0\DataNormalizer.Generators.dll"
Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
Directory.Build.props
Shared settings applied to ALL projects in the repo:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>
Rules:
- Never override these settings in individual
.csproj files unless absolutely necessary
- The generator project may need additional settings (e.g.,
EnforceExtendedAnalyzerRules)
- Test projects may need
<IsPackable>false</IsPackable>
Clean Architecture Principles
Layer Separation
-
Runtime Library (DataNormalizer): Contains only the public API surface โ attributes, configuration types, and runtime containers. Zero external dependencies. This is what consumers interact with at runtime.
-
Source Generator (DataNormalizer.Generators): All code analysis and generation logic. Never referenced at runtime. Targets netstandard2.0 per Roslyn requirements. Uses PolySharp for C# 12 features on netstandard2.0.
-
Tests: Each test project tests a specific layer. Generator tests use Verify for snapshots. Integration tests exercise the full pipeline with real generated code.
Dependency Direction
Dependencies point inward:
- Tests โ Runtime / Generator
- Samples โ Runtime
- Generator โ Runtime (conceptually: it knows about runtime types to generate code that uses them)
- Runtime โ nothing
No Cross-Contamination
- Runtime types NEVER reference
Microsoft.CodeAnalysis
- Generator code NEVER references test frameworks
- Test utilities are in test projects, not runtime
- Sample code demonstrates consumer usage, not internal APIs
MSBuild Standards
Project File Organization
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\DataNormalizer\DataNormalizer.csproj" />
</ItemGroup>
</Project>
Rules
PropertyGroup comes first, then ItemGroups
- Group
ItemGroups logically: packages, then project references, then content items
- Never include package versions in
.csproj โ they belong in Directory.Packages.props
- Use relative paths with backslashes for
ProjectReference (MSBuild convention)
- Test projects: always set
<IsPackable>false</IsPackable>
Folder Organization Within Projects
Runtime Library
DataNormalizer/
โโโ Attributes/ # [NormalizeConfiguration], [NormalizeIgnore], etc.
โโโ Configuration/ # NormalizationConfig, NormalizeBuilder, etc.
โโโ Runtime/ # NormalizationContext
โโโ DataNormalizer.csproj
Generator
DataNormalizer.Generators/
โโโ Analysis/ # Parsing and analysis (ConfigurationParser, TypeGraphAnalyzer)
โโโ Emitters/ # Code generation (DtoEmitter, NormalizerEmitter, etc.)
โโโ Models/ # Immutable pipeline data (records, never SemanticModel)
โโโ Diagnostics/ # DiagnosticDescriptors
โโโ NormalizeGenerator.cs
โโโ DataNormalizer.Generators.csproj
Test Projects
DataNormalizer.Tests/
โโโ Attributes/ # Mirror runtime structure
โโโ Configuration/
โโโ Runtime/
โโโ DataNormalizer.Tests.csproj
DataNormalizer.Generators.Tests/
โโโ Analysis/
โโโ Emitters/
โโโ Snapshots/ # .verified.cs files for Verify
โโโ DataNormalizer.Generators.Tests.csproj
Adding New Types Checklist
- Create the file in the correct folder matching the namespace
- Use file-scoped namespace
- Filename matches type name exactly
- Add
sealed unless designed for inheritance
- Add appropriate access modifier (
public for API surface, internal for implementation)
- If it needs a package reference, add to
Directory.Packages.props first
- Run
dotnet build to verify
- Run
dotnet csharpier format . before committing