Engineering Notes
Home/Languages/C#/.NET Project Structure
Languages · C#

.NET Project Structure

A typical layered backend:

💻 Languages● Advanced⏱ 4 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Solutions, Projects, Assemblies
  • The SDK-Style .csproj
  • The CLI Workflow
  • Dependency Direction
  • Visibility Across Assemblies

1Solutions, Projects, Assemblies

ThingFileWhat it is
Solution*.sln / *.slnxa list of projects that are opened/built together. Not a build unit itself.
Project*.csprojMSBuild file: what to compile, target framework, dependencies. One project → one assembly.
Assembly*.dll / *.execompiled IL + metadata — the unit of deployment, versioning and internal visibility.
Namespacenamespace X.Y;a naming scope only. Unrelated to folders or assemblies (by convention it mirrors them).

A typical layered backend:

text
Shop.sln
src/
  Shop.Api/            Shop.Api.csproj           (ASP.NET Core host, controllers)
  Shop.Application/    Shop.Application.csproj   (use cases, interfaces)
  Shop.Domain/         Shop.Domain.csproj        (entities, no dependencies)
  Shop.Infrastructure/ Shop.Infrastructure.csproj(EF Core, external APIs)
tests/
  Shop.Domain.Tests/   Shop.Domain.Tests.csproj  (xUnit)
Directory.Build.props   shared MSBuild settings for every project below it
global.json             pins the SDK version

2The SDK-Style .csproj

xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>              <!-- Library (default) or Exe -->
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <RootNamespace>Shop.Api</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
    <ProjectReference Include="..\Shop.Application\Shop.Application.csproj" />
  </ItemGroup>
</Project>

SDK-style projects glob every **/*.cs under the folder automatically — you don't list source files. Sdk="Microsoft.NET.Sdk.Web" adds ASP.NET Core references and defaults.

3The CLI Workflow

bash
dotnet new sln -n Shop
dotnet new classlib -o src/Shop.Domain
dotnet new webapi   -o src/Shop.Api
dotnet new xunit    -o tests/Shop.Domain.Tests
dotnet sln add src/Shop.Domain src/Shop.Api tests/Shop.Domain.Tests

dotnet add src/Shop.Api reference src/Shop.Domain     # ProjectReference
dotnet add src/Shop.Api package Serilog.AspNetCore     # PackageReference

dotnet restore   # resolve NuGet graph -> obj/project.assets.json
dotnet build     # compile -> bin/Debug/net8.0/
dotnet test
dotnet run --project src/Shop.Api
dotnet publish -c Release -o out   # deployable output

4Dependency Direction

Project references form a directed acyclic graph — MSBuild refuses cycles. That makes the .csproj graph a cheap, compiler-enforced architecture rule: if Shop.Domain has no references, domain code cannot accidentally depend on EF Core or ASP.NET.

text
Api ──► Application ──► Domain
 └────► Infrastructure ─┘ (implements Application's interfaces)

5Visibility Across Assemblies

internal means "visible inside this assembly only." Tests sometimes need internals:

xml
<ItemGroup>
  <InternalsVisibleTo Include="Shop.Domain.Tests" />
</ItemGroup>

6Configuration and Entry Point

Modern apps use top-level statements in Program.cs (the compiler generates the Main method) and layered configuration: appsettings.json → appsettings.{Environment}.json → environment variables → command line, later sources overriding earlier ones.

csharp
var builder = WebApplication.CreateBuilder(args);
var conn = builder.Configuration.GetConnectionString("Default");
var app = builder.Build();
app.MapGet("/health", () => "ok");
app.Run();
Under the HoodRestore, Build, and What Lands in bin/▾
  1. Restore walks every PackageReference and ProjectReference transitively, resolves one version per package (nearest-wins; the lowest version that satisfies the range), downloads into the global cache (~/.nuget/packages) and writes obj/project.assets.json — the lock-like graph the build reads.
  2. Build runs MSBuild targets: CoreCompile invokes Roslyn (csc) with every source file and the resolved reference assemblies, producing an IL assembly in obj/, then copies it plus dependencies into bin/<Configuration>/<TFM>/.
  3. Alongside your .dll, the output has *.deps.json (dependency manifest the host uses to locate assemblies) and, for executables, *.runtimeconfig.json (which runtime version to load) and an apphost (Shop.Api / Shop.Api.exe).
  4. At run time the host (dotnet muxer → hostfxr → hostpolicy) reads those JSON files, loads the CoreCLR, and the JIT compiles IL methods to native code the first time each is called.
dotnet restore resolves the NuGet graph into obj/project.assets.json, Roslyn compiles sources into an IL assembly, and the host uses deps.json and runtimeconfig.json to load the runtime and JIT the IL
dotnet restore resolves the NuGet graph into obj/project.assets.json, Roslyn compiles sources into an IL assembly, and the host uses deps.json and runtimeconfig.json to load the runtime and JIT the IL

⚠️ Common Pitfalls

  1. Circular project references — MSBuild errors out. Extract the shared piece into a lower project or introduce an interface in the lower layer.
  2. Mismatched TargetFrameworks — a net8.0 project can't reference a net9.0 library.
  3. Editing files under bin/ or obj/ — they're regenerated; never commit them (dotnet new gitignore).
  4. Package versions drifting between projects — use Central Package Management (Directory.Packages.props + ManagePackageVersionsCentrally) so every project agrees.
  5. Putting secrets in appsettings.json — use user-secrets in dev and environment variables / a vault in production.

📋 Quick Reference

CommandEffect
dotnet new <template> -o <dir>scaffold a project
dotnet sln add <proj>add to solution
dotnet add reference <proj>project-to-project dependency
dotnet add package <id>NuGet dependency
dotnet build / test / runcompile / test / execute
dotnet publish -c Releasedeployable output
Directory.Build.propsshared settings for a folder tree
global.jsonpin the SDK version

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1What are solutions, projects and assemblies?core▾

A solution (.sln/.slnx) groups projects; each project (.csproj) builds an assembly (DLL/EXE). Project references define dependencies; NuGet packages bring external libraries.

Follow-ups interviewers ask
  • SDK-style projects? Concise csproj with implicit file globbing.
Q2How would you structure a clean architecture solution?core▾

Projects such as Domain (entities, no dependencies), Application (use cases, interfaces), Infrastructure (EF Core, external services), Api (ASP.NET Core), plus test projects; references point inward.

Follow-ups interviewers ask
  • Enforce boundaries? Project references and NetArchTest architecture tests.
Q3What's Directory.Build.props and central package management?core▾

Directory.Build.props shares MSBuild settings (nullable, warnings as errors, analyzers) across projects; Directory.Packages.props centralizes NuGet versions so all projects use the same versions.

Follow-ups interviewers ask
  • Why centralize versions? Avoid version drift and conflicts.
Q4How does configuration work in .NET?senior▾

Layered providers: appsettings.json, appsettings.{Environment}.json, user secrets (dev), environment variables, command line, Key Vault; bound to typed classes with the Options pattern and validated at startup.

Follow-ups interviewers ask
  • ValidateOnStart? Fails fast on invalid configuration.
Q5What deployment options exist for .NET apps?core▾

Framework-dependent or self-contained publishes, single-file executables, ReadyToRun or Native AOT for faster startup, and container images (dotnet publish /t:PublishContainer).

Follow-ups interviewers ask
  • Native AOT trade-offs? Fast startup and small memory, but limited reflection/dynamic code.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.