Most people copy a .csproj from the last project and never look at it again. It's the file you generate once with dotnet new webapi and then ignore for the rest of the project's life. I think that's a mistake: a handful of settings in there decide how much your build actually catches before broken code reaches production.
Before getting to those settings, it's worth clearing up something a lot of people are fuzzy on: the difference between the SDK and the Runtime.
SDK vs Runtime
The .NET download page gives you two options, and a lot of beginners aren't sure which one they need.
The SDK is the full developer toolkit. It includes the Runtime, the compiler, the dotnet CLI, build tools, and project templates. If you're writing code, this is what you install.
The Runtime is smaller and only runs applications. You can't create a project with it, build one, or restore NuGet packages. It's meant for production servers where the app has already been built elsewhere. These days a lot of production setups just use a Docker image with the Runtime baked in, so even that distinction shows up less than it used to.
Easy way to remember it: developers install the SDK, production servers usually need only the Runtime.
One thing that makes this less painful than it sounds: the SDK has roll-forward support. Install the .NET 10 SDK and it can still build projects targeting net8.0 or net9.0. You don't need five SDK versions installed just because you're bouncing between older repos.
Why this matters right now
Microsoft ships two kinds of releases: LTS (Long-Term Support) and STS (Standard-Term Support). LTS versions get support for several years and are what most companies run in production. .NET 8 and .NET 10 fall into this category. STS versions get new features sooner but with a shorter support window, which is fine if your team upgrades often and less fine if it doesn't.
For most enterprise applications, the answer is simple: use the latest LTS.
.NET 8 and .NET 9 both reach end of support on November 10, 2026. If you're running either in production, that's the actual deadline to have an upgrade plan. And if you're going to touch the project anyway to bump the target framework, that's the moment to also look at what else is sitting in the .csproj that you never configured on purpose.
The settings that actually matter
A typical .csproj looks something like this:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<AnalysisLevel>latest-recommended</AnalysisLevel>
</PropertyGroup>
</Project>
ImplicitUsings is the least consequential of the group. It just means you stop writing using System;, using System.Linq;, and so on at the top of every file, since the common namespaces get included automatically. Nice, but cosmetic.
The rest change what your build is willing to tolerate.
TreatWarningsAsErrors turns a warning from something you can ignore into something that stops the build cold. Normally a warning is just noise in the output and the project compiles anyway. With this on, a warning fails the build the same way a compile error would. Warnings usually point at real problems, and this setting forces you to deal with them the moment they appear instead of letting them pile up as debt someone else has to clean up later.
EnforceCodeStyleInBuild checks your style rules (the ones defined in .editorconfig) at build time, not just when someone happens to run a linter. That means the whole team follows the same conventions automatically, because the build won't let inconsistent formatting through.
AnalysisLevel set to latest-recommended turns on the newest Roslyn analyzers your SDK version supports. These catch things beyond simple syntax problems: possible bugs, performance issues, API misuse, and other patterns that are technically valid C# but bad practice. Catching that at build time, before the code ships, is a lot cheaper than catching it in production.
None of these are exotic. They're one line each in a PropertyGroup. But the difference between a build that just compiles your code and a build that actively checks it is exactly these three lines.
Repository-wide, not per-project
If you're running more than one project in a solution, repeating these settings in every .csproj gets old fast, and it's easy for them to drift out of sync. Directory.Build.props at the repo root lets you define shared settings once and have every project pick them up.
The same idea applies to package versions. Instead of each project picking its own version of a NuGet package, Directory.Packages.props centralizes that in one place: fewer version conflicts, easier upgrades, and everyone in the solution referencing the same version of the same package.
Both of these are about whether the build system does useful work on your behalf before your code goes anywhere near production. Worth checking what's actually in that file next time you open a project, especially one still targeting .NET 8 or 9 with that November 2026 deadline on the calendar.
Top comments (0)