Project Structure in .NET 8: Solutions, .csproj, NuGet, and Build Outputs
Published:
This post covers how a typical .NET codebase is organized. New developers often focus on Program.cs and controllers, but production projects are shaped just as much by how solutions are split, how project files are configured, how packages are restored, and where compiled artifacts are written. If you understand those four pieces, you can navigate almost any .NET repository with less guesswork.
What a .NET solution is
A solution (.sln) is a container for one or more projects. It does not compile code by itself. Instead, it helps you group projects, manage references between them, and open the whole workspace in Visual Studio or the CLI.
Common solution members:
- an ASP.NET Core Web API
- one or more class libraries
- test projects
- worker services or console apps
- shared build configuration files
A realistic setup might look like:
StoreApp/
StoreApp.sln
src/
StoreApp.Api/
StoreApp.Api.csproj
StoreApp.Application/
StoreApp.Application.csproj
StoreApp.Domain/
StoreApp.Domain.csproj
StoreApp.Infrastructure/
StoreApp.Infrastructure.csproj
tests/
StoreApp.Application.Tests/
StoreApp.Application.Tests.csproj
The solution makes it easy to build everything with one command:
dotnet build StoreApp.sln
Creating a solution from the CLI
The .NET CLI can build your initial structure in a few minutes:
dotnet new sln -n StoreApp
dotnet new webapi -n StoreApp.Api -o src/StoreApp.Api
dotnet new classlib -n StoreApp.Application -o src/StoreApp.Application
dotnet new classlib -n StoreApp.Domain -o src/StoreApp.Domain
dotnet new classlib -n StoreApp.Infrastructure -o src/StoreApp.Infrastructure
dotnet new xunit -n StoreApp.Application.Tests -o tests/StoreApp.Application.Tests
dotnet sln StoreApp.sln add src/StoreApp.Api/StoreApp.Api.csproj
dotnet sln StoreApp.sln add src/StoreApp.Application/StoreApp.Application.csproj
dotnet sln StoreApp.sln add src/StoreApp.Domain/StoreApp.Domain.csproj
dotnet sln StoreApp.sln add src/StoreApp.Infrastructure/StoreApp.Infrastructure.csproj
dotnet sln StoreApp.sln add tests/StoreApp.Application.Tests/StoreApp.Application.Tests.csproj
Now add project references so the application layers can talk to each other explicitly:
dotnet add src/StoreApp.Api/StoreApp.Api.csproj reference src/StoreApp.Application/StoreApp.Application.csproj
dotnet add src/StoreApp.Application/StoreApp.Application.csproj reference src/StoreApp.Domain/StoreApp.Domain.csproj
dotnet add src/StoreApp.Infrastructure/StoreApp.Infrastructure.csproj reference src/StoreApp.Application/StoreApp.Application.csproj
This is one of the most important habits in .NET: make dependencies visible through project references instead of hidden through copied code.
Understanding the .csproj file
Every .NET project has a project file, usually ending in .csproj. This XML file tells the SDK:
- which target framework to compile for
- whether the output is an executable or library
- which NuGet packages are required
- which other projects are referenced
- which build settings should be enabled
A simple Web API project file looks like:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>StoreApp.Api</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StoreApp.Application\StoreApp.Application.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>
Important properties to recognize:
Sdkchooses the build logic. Web projects usually useMicrosoft.NET.Sdk.Web.TargetFrameworkdefines the runtime contract, such asnet8.0.Nullableenables compile-time null safety.ImplicitUsingsreduces repetitiveusingstatements.OutputTypeappears in console apps and determines whether the project produces an executable.
Project references vs NuGet packages
A .csproj can depend on code in two main ways.
Use a ProjectReference when:
- the code lives in the same repository
- you want changes to compile together
- you control both sides of the dependency
Use a PackageReference when:
- the dependency is versioned and distributed as a package
- the code comes from a third-party library
- the dependency should be shared across many repos
Example:
<ItemGroup>
<ProjectReference Include="..\StoreApp.Domain\StoreApp.Domain.csproj" />
<PackageReference Include="Dapper" Version="2.1.35" />
</ItemGroup>
That distinction matters. Project references give you source-level integration. NuGet packages give you versioned distribution.
How NuGet fits into the build
NuGet is the package manager for .NET. When you run dotnet restore, the CLI:
- reads all
PackageReferenceitems - resolves dependency versions
- downloads missing packages
- writes restore metadata into the
objfolder
Common package commands:
dotnet add package Serilog.AspNetCore
dotnet remove package Serilog.AspNetCore
dotnet restore
dotnet list package
In modern SDK-style projects, dotnet build automatically performs restore if needed. Still, it is useful to know that restore is a separate phase, especially in CI pipelines.
What lives in bin and obj
Two folders appear after you build a project:
obj/contains intermediate build databin/contains the final compiled output
Example output after dotnet build:
src/StoreApp.Api/
bin/
Debug/
net8.0/
StoreApp.Api.dll
StoreApp.Api.exe
appsettings.json
Swashbuckle.AspNetCore.dll
obj/
Debug/
net8.0/
StoreApp.Api.AssemblyInfo.cs
project.assets.json
What these folders are for:
objis used by MSBuild during compilation. It holds generated files and restore state.binis what you normally run, inspect, or publish.
Important files you may see:
project.assets.jsoninobjtracks resolved NuGet dependencies.dllfiles inbinare your compiled assemblies.pdbfiles contain debug symbols- copied configuration files, such as
appsettings.json, appear in output if marked appropriately
Debug vs Release builds
By default, dotnet build creates a Debug build. Debug is optimized for development, not performance. Release is optimized for deployment.
dotnet build -c Debug
dotnet build -c Release
The folder structure reflects that choice:
bin/Debug/net8.0/
bin/Release/net8.0/
When you are troubleshooting locally, Debug is fine. When you are benchmarking or preparing deployment artifacts, build or publish in Release.
Build vs publish
New developers often confuse build and publish.
dotnet build:
- compiles the code
- produces assemblies in
bin - is intended mainly for development and CI validation
dotnet publish:
- prepares a deployable output
- copies only what is needed to run the app
- can generate framework-dependent or self-contained output
Example:
dotnet publish src/StoreApp.Api/StoreApp.Api.csproj -c Release -o publish
That publish folder is what you usually ship to a server, container build, or deployment pipeline.
A practical multi-project example
Suppose your API needs a service and a domain model:
namespace StoreApp.Domain;
public sealed record Product(int Id, string Name, decimal Price);
namespace StoreApp.Application;
using StoreApp.Domain;
public interface IProductService
{
IReadOnlyList<Product> GetAll();
}
public sealed class ProductService : IProductService
{
public IReadOnlyList<Product> GetAll() =>
[
new Product(1, "Mechanical Keyboard", 129.00m),
new Product(2, "USB-C Dock", 89.00m)
];
}
Your API project can reference the application project and inject the service:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();
app.MapGet("/products", (IProductService service) => Results.Ok(service.GetAll()));
app.Run();
That separation is only possible because the solution and project references describe the structure clearly.
Useful build files beyond .csproj
As your repo grows, you will see more build-related files:
global.jsonlocks the SDK versionDirectory.Build.propscentralizes shared MSBuild propertiesDirectory.Packages.propscentralizes NuGet package versionsNuGet.configcustomizes package sources
You do not need all of them on day one, but you should recognize their purpose when you join an existing team.
Best practices for project structure
A solid baseline looks like this:
- keep one solution at the repo root
- group production code under
src/ - group tests under
tests/ - keep project names aligned with folders and namespaces
- prefer small, focused class libraries over one giant “shared” project
- use project references for internal code and package references for external dependencies
- avoid committing
bin/andobj/to source control
If you understand the structure, you can answer questions faster:
- Where does this type come from?
- Which project owns this dependency?
- Why did restore fail?
- Which folder contains the files that will actually deploy?
Next Article: C# Essentials for .NET Developers: Types, LINQ, and Async/Await
