Learnixo
Back to blog
AI Systemsintermediate

Project Structure — 8 Projects, the .slnx Format, and What Goes Where

The exact project layout of a Clean Architecture .NET solution: what each of the 8 projects contains, why the .slnx format replaces .sln, and the conventions that keep the solution navigable.

Asma Hafeez KhanMay 16, 20266 min read
Clean Architecture.NETProject StructureSolutionOrganization
Share:š•

The Full Directory Layout

SystemForge/
ā”œā”€ā”€ SystemForge.slnx                        ← XML solution format (replaces .sln)
│
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ SystemForge.Domain/
│   │   ā”œā”€ā”€ AssemblyReference.cs
│   │   ā”œā”€ā”€ Entities/
│   │   │   └── Patient.cs
│   │   ā”œā”€ā”€ ValueObjects/
│   │   │   └── PatientId.cs
│   │   ā”œā”€ā”€ Events/
│   │   │   └── PatientCreatedDomainEvent.cs
│   │   └── SystemForge.Domain.csproj
│   │
│   ā”œā”€ā”€ SystemForge.Application/
│   │   ā”œā”€ā”€ AssemblyReference.cs
│   │   ā”œā”€ā”€ Patients/
│   │   │   ā”œā”€ā”€ Commands/
│   │   │   │   ā”œā”€ā”€ CreatePatient/
│   │   │   │   │   ā”œā”€ā”€ CreatePatientCommand.cs
│   │   │   │   │   ā”œā”€ā”€ CreatePatientCommandHandler.cs
│   │   │   │   │   └── CreatePatientCommandValidator.cs
│   │   │   └── Queries/
│   │   │       └── GetPatient/
│   │   │           ā”œā”€ā”€ GetPatientQuery.cs
│   │   │           ā”œā”€ā”€ GetPatientQueryHandler.cs
│   │   │           └── PatientResponse.cs
│   │   ā”œā”€ā”€ Abstractions/
│   │   │   ā”œā”€ā”€ IPatientRepository.cs
│   │   │   └── IEmailService.cs
│   │   ā”œā”€ā”€ Common/
│   │   │   └── Result.cs
│   │   └── SystemForge.Application.csproj
│   │
│   ā”œā”€ā”€ SystemForge.Infrastructure/
│   │   ā”œā”€ā”€ AssemblyReference.cs
│   │   ā”œā”€ā”€ Persistence/
│   │   │   ā”œā”€ā”€ AppDbContext.cs
│   │   │   ā”œā”€ā”€ Configurations/
│   │   │   │   └── PatientConfiguration.cs
│   │   │   └── Migrations/
│   │   ā”œā”€ā”€ Repositories/
│   │   │   └── PatientRepository.cs
│   │   ā”œā”€ā”€ Services/
│   │   │   └── EmailService.cs
│   │   ā”œā”€ā”€ Identity/
│   │   │   ā”œā”€ā”€ AppUser.cs
│   │   │   └── TokenService.cs
│   │   └── SystemForge.Infrastructure.csproj
│   │
│   ā”œā”€ā”€ SystemForge.Api/
│   │   ā”œā”€ā”€ Controllers/
│   │   │   └── PatientsController.cs
│   │   ā”œā”€ā”€ Middleware/
│   │   │   └── ExceptionHandlingMiddleware.cs
│   │   ā”œā”€ā”€ Extensions/
│   │   │   ā”œā”€ā”€ ServiceCollectionExtensions.cs
│   │   │   └── WebApplicationExtensions.cs
│   │   ā”œā”€ā”€ Program.cs
│   │   └── SystemForge.Api.csproj
│   │
│   ā”œā”€ā”€ SystemForge.AppHost/
│   │   ā”œā”€ā”€ Program.cs
│   │   └── SystemForge.AppHost.csproj
│   │
│   └── SystemForge.ServiceDefaults/
│       ā”œā”€ā”€ Extensions.cs
│       └── SystemForge.ServiceDefaults.csproj
│
└── tests/
    ā”œā”€ā”€ SystemForge.Architecture.Tests/
    │   ā”œā”€ā”€ LayerDependencyTests.cs
    │   └── SystemForge.Architecture.Tests.csproj
    └── SystemForge.Application.UnitTests/
        ā”œā”€ā”€ Patients/
        │   └── CreatePatientCommandHandlerTests.cs
        └── SystemForge.Application.UnitTests.csproj

The .slnx Format

The .slnx format is a clean XML replacement for the legacy .sln text format introduced in .NET 9.

XML
<!-- SystemForge.slnx -->
<Solution>
  <Folder Name="/src/">
    <Project Path="src/SystemForge.Domain/SystemForge.Domain.csproj" />
    <Project Path="src/SystemForge.Application/SystemForge.Application.csproj" />
    <Project Path="src/SystemForge.Infrastructure/SystemForge.Infrastructure.csproj" />
    <Project Path="src/SystemForge.Api/SystemForge.Api.csproj" />
    <Project Path="src/SystemForge.AppHost/SystemForge.AppHost.csproj" />
    <Project Path="src/SystemForge.ServiceDefaults/SystemForge.ServiceDefaults.csproj" />
  </Folder>
  <Folder Name="/tests/">
    <Project Path="tests/SystemForge.Architecture.Tests/SystemForge.Architecture.Tests.csproj" />
    <Project Path="tests/SystemForge.Application.UnitTests/SystemForge.Application.UnitTests.csproj" />
  </Folder>
</Solution>
Advantages over .sln:
  āœ“ Human-readable XML (no GUID noise)
  āœ“ Diff-friendly — merge conflicts are obvious
  āœ“ Sorted and consistent — no tool-specific section ordering
  āœ“ Supported by Visual Studio 2022 17.10+, Rider, and `dotnet` CLI

Each Project's Responsibilities

C#
// ─── Domain ────────────────────────────────────────────────────────────────
// What goes here: entities, value objects, domain events, domain exceptions
// What does NOT go here: anything with a NuGet dependency

// Domain/Entities/Patient.cs
public sealed class Patient
{
    public PatientId Id { get; private set; }
    public string Name { get; private set; }
    public DateOnly DateOfBirth { get; private set; }

    private readonly List<IDomainEvent> _domainEvents = [];

    private Patient() { }   // EF Core constructor

    public static Patient Create(string name, DateOnly dob)
    {
        var patient = new Patient
        {
            Id   = PatientId.New(),
            Name = name,
            DateOfBirth = dob,
        };
        patient._domainEvents.Add(new PatientCreatedDomainEvent(patient.Id));
        return patient;
    }

    public IReadOnlyList<IDomainEvent> PopDomainEvents()
    {
        var events = _domainEvents.ToList();
        _domainEvents.Clear();
        return events;
    }
}
C#
// ─── Application ───────────────────────────────────────────────────────────
// What goes here: command/query handlers, interfaces (IRepository, IEmailService),
//                 validators, response DTOs, Result<T>
// What does NOT go here: EF Core, SqlConnection, HttpClient, anything I/O

// Application/Abstractions/IPatientRepository.cs
public interface IPatientRepository
{
    Task<Patient?> GetByIdAsync(PatientId id, CancellationToken ct);
    Task AddAsync(Patient patient, CancellationToken ct);
    Task<bool> ExistsByNameAsync(string name, CancellationToken ct);
}

// Application/Patients/Commands/CreatePatient/CreatePatientCommandHandler.cs
public sealed class CreatePatientCommandHandler
{
    private readonly IPatientRepository _patients;

    public CreatePatientCommandHandler(IPatientRepository patients)
        => _patients = patients;

    public async Task<Result<PatientId>> Handle(
        CreatePatientCommand command, CancellationToken ct)
    {
        if (await _patients.ExistsByNameAsync(command.Name, ct))
            return Result.Failure<PatientId>(PatientErrors.NameTaken);

        var patient = Patient.Create(command.Name, command.DateOfBirth);
        await _patients.AddAsync(patient, ct);
        return Result.Success(patient.Id);
    }
}
C#
// ─── Infrastructure ────────────────────────────────────────────────────────
// What goes here: EF Core DbContext, concrete repository implementations,
//                 external API clients, email/SMS, identity, Redis clients
// What does NOT go here: business logic

// Infrastructure/Persistence/PatientRepository.cs
public sealed class PatientRepository : IPatientRepository
{
    private readonly AppDbContext _context;

    public PatientRepository(AppDbContext context) => _context = context;

    public async Task<Patient?> GetByIdAsync(PatientId id, CancellationToken ct)
        => await _context.Patients.FirstOrDefaultAsync(p => p.Id == id, ct);

    public async Task AddAsync(Patient patient, CancellationToken ct)
        => await _context.Patients.AddAsync(patient, ct);

    public async Task<bool> ExistsByNameAsync(string name, CancellationToken ct)
        => await _context.Patients.AnyAsync(p => p.Name == name, ct);
}
C#
// ─── Api ───────────────────────────────────────────────────────────────────
// What goes here: controllers/minimal API endpoints, middleware, DI wiring,
//                 request/response mapping, Program.cs
// What does NOT go here: business logic, direct DB access

// Api/Controllers/PatientsController.cs
[ApiController]
[Route("api/patients")]
public sealed class PatientsController : ControllerBase
{
    private readonly CreatePatientCommandHandler _create;
    private readonly GetPatientQueryHandler _get;

    public PatientsController(
        CreatePatientCommandHandler create,
        GetPatientQueryHandler get)
    {
        _create = create;
        _get    = get;
    }

    [HttpPost]
    public async Task<IActionResult> Create(
        CreatePatientRequest request, CancellationToken ct)
    {
        var command = new CreatePatientCommand(request.Name, request.DateOfBirth);
        var result = await _create.Handle(command, ct);
        return result.Match<IActionResult>(
            id => CreatedAtAction(nameof(Get), new { id }, id),
            err => Problem(err.Description));
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(Guid id, CancellationToken ct)
    {
        var query = new GetPatientQuery(new PatientId(id));
        var result = await _get.Handle(query, ct);
        return result.Match<IActionResult>(Ok, err => NotFound(err.Description));
    }
}

AssemblyReference Convention

Each project exposes a single empty class as an anchor for assembly discovery:

C#
// Domain/AssemblyReference.cs
namespace SystemForge.Domain;

public static class AssemblyReference { }

// Application/AssemblyReference.cs
namespace SystemForge.Application;

public static class AssemblyReference { }

This lets architecture tests, DI scanning, and EF Core migrations reference the assembly without knowing a specific type:

C#
// Architecture.Tests/LayerDependencyTests.cs
var domainAssembly = typeof(Domain.AssemblyReference).Assembly;

// Infrastructure — EF Core migration target
dotnet ef migrations add Init --project src/Infrastructure --startup-project src/Api

Project File Conventions

XML
<!-- Domain.csproj — minimal, no NuGet packages -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

<!-- Application.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="FluentValidation" Version="11.*" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\Domain\SystemForge.Domain.csproj" />
  </ItemGroup>
</Project>

TreatWarningsAsErrors at the project level is the simplest way to maintain code quality — it is more reliable than .editorconfig alone and is enforced at build time, not just in the IDE.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.