.NET Project Setup - Best Practices

TL;DR

To implement a clean directory layout you can set UseArtifactsOutput (.NET Artifacts Output property) or set BaseIntermediateOutputPath and BaseOutputPath.

Whether you are starting out with a new project or refactoring a mature codebase, a good project and folder organization makes your life easier.

There are many good articles about how to structure the content of the source code directory, but what about the intermediate and binary directories? .NET still has the default convention of mixing source code with the generated build artifacts, namely putting an obj (intermediate) and bin (final binary output) folder inside each project directory. A widely used structure separates tests from program code, resulting in a hirarchy like this

Mixed directory layout (.NET default)
demo-app/
├─ src
│  ├─ Project1.csproj
│  │  ├─ bin
│  │  ├─ obj
│  │  └─ Demo.cs
│  └─ ...
├─ test
│  ├─ Project1Test.csproj
│  │  ├─ bin
│  │  ├─ obj
│  │  └─ DemoTest.cs
│  └─ ...
├─ Demo.sln
└─ ...

While it is important to keep the generated output of each project in a separate folder to avoid build problems there is still the problem that we have generated and binary content in our source code directories (src and test). Imagine a larger project with 100+ projects inside a solution and there is some error with the build tooling (while the clean command became quite reliable it can still contain defects). Manually cleaning all 100+ folders is a tedious task and takes a lot of time! Another reason are custom build actions which become less error prone since they normally operate on the intermediate or binary output reducing also the potential to mistakenly change source files.

A better layout separates the build artifacts from the sources like this

Clean directory layout
demo-app/
├─ bin
│  ├─ Project1
│  └─ Project1Test
├─ obj
│  ├─ Project1
│  └─ Project1Test
├─ src
│  ├─ Project1.csproj
│  │  └─ Demo.cs
│  └─ ...
├─ test
│  ├─ Project1Test.csproj
│  │  └─ DemoTest.cs
│  └─ ...
├─ Demo.sln
└─ ...

The location of the bin and obj folders can be changed in your .csproj or by adding a Directory.Build.Props alongside your .sln file by setting the BaseOutputPath and BaseIntermediateOutputPath. At the time of this writing the BaseIntermediateOutputPath needs to be set before the import of the Microsoft.Common.Props otherwise some NuGet assets will still be placed in the standard directory. You can either change from the implicit to the explicit project format or use the Directory.Build.Props file (recommended) which is always evaluated first.

Directory.Build.Props file next to .sln file.
<Project>
	<PropertyGroup>
		<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
	</PropertyGroup>
</Project>
Implicit imports model to explicit imports
-<Project Sdk="Microsoft.NET.Sdk">
+<Project>
	<PropertyGroup>
		<BaseIntermediateOutputPath>..\..\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
	</PropertyGroup>
+	<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
	...
+	<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>