Hello again. In the previous lesson, you chose net10.0 as the supported, cross-platform target for the game-backend project. That decision becomes useful only when it is applied consistently: an API, domain library, infrastructure library, and test projects should not quietly drift into different compiler and quality settings.
This lesson establishes that shared baseline. You will create a repository-level Directory.Build.props file, enable nullable reference types, choose a practical built-in analyzer policy, and connect those build settings to an .editorconfig rule policy. The goal is not to make a build fail over every possible stylistic preference. It is to make important correctness and maintainability feedback visible early and consistently, on every developer machine and in CI.
One repository, one baseline
A typical project layout for the portfolio backend will soon look like this:
game-backend/
├── Directory.Build.props
├── .editorconfig
├── GameBackend.sln
├── src/
│ ├── GameBackend.Api/
│ ├── GameBackend.Player/
│ ├── GameBackend.Progress/
│ └── GameBackend.Infrastructure/
└── tests/
├── GameBackend.Player.Tests/
└── GameBackend.Api.IntegrationTests/
A project file is still the right place for facts that are truly specific to one project: an ASP.NET Core project SDK, a database package, an executable output type, or a special publish configuration. It is a poor place to repeat policies that should be the same everywhere.
Directory.Build.props is an MSBuild convention. MSBuild searches upward from each project directory, finds the nearest file with that name, and imports it early while evaluating the project. A file placed at the repository root therefore provides defaults to every project under src and tests.
Customize the build by folder - MSBuild | Microsoft Learn
Read Microsoft Learn’s “Customize the build by folder” to understand the scope and import behavior behind Directory.Build.props. This explains why the file belongs at the repository root rather than beside the solution file by convention alone.
Start with the Quick reference table, then read Directory.Build.props and Directory.Build.targets. Focus on the shared import behavior, including why props files are imported early. In Search scope, follow the directory search example. Then read Import order, paying particular attention to local overrides.
Three operational details matter:
- The solution-file location does not control discovery. MSBuild starts at the individual project’s location and walks upward.
- The nearest
Directory.Build.propswins. If someone later createssrc/Directory.Build.props, it does not automatically merge with the root file. Nested files need an explicit import of the outer policy if both are intended to apply. - The filename is case-sensitive on Linux. Use exactly
Directory.Build.props. This matters for the Linux containers planned for Azure and for Linux CI agents.
The word defaults is important. A property set in a project file later in evaluation can override a property from Directory.Build.props. That is sometimes appropriate, but an exception should be deliberate and visible in code review. If every project overrides the central setting, the central policy is not actually a policy.
Add the shared build policy
Create Directory.Build.props at the repository root:
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AnalysisMode>Recommended</AnalysisMode>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>
This is intentionally a compact baseline. Each setting has a distinct role.
| Setting | Effect | Why it belongs in the shared policy |
|---|---|---|
TargetFramework | Compiles all ordinary projects against .NET 10 | Retains the supported target decision from the previous lesson |
ImplicitUsings | Makes SDK-provided global using directives available | Avoids repetitive imports while remaining consistent across projects |
Nullable | Enables nullable reference type analysis | Makes nullability part of the source-level contract |
AnalysisMode | Selects the SDK analyzer rule set | Enables a useful built-in quality baseline |
EnforceCodeStyleInBuild | Allows configured code-style diagnostics to participate in builds | Keeps IDE feedback and CI feedback aligned |
TreatWarningsAsErrors | Fails a build when a warning is emitted | Prevents the normalisation of a growing warning backlog |
Target framework: centralize carefully
It is reasonable to place TargetFramework here because the planned API, libraries, and tests are all part of one .NET 10 system. This lets a framework upgrade become a reviewed repository-level change rather than a hunt through many .csproj files.
Do not treat this as a requirement that every repository must centralize its target framework. A solution with an older migration utility, a source generator, or a reusable library that intentionally supports another target may need a project-specific TFM. The central file should express the normal case, not conceal real compatibility constraints.
Language version: do not casually set latest
You may see examples that add this:
<LangVersion>latest</LangVersion>
Avoid it for this project. With modern SDK-style projects, C# selects a language version compatible with the target framework by default. Because the project targets net10.0, that default is the intended modern C# baseline.
latest means “whatever the installed SDK currently considers latest.” That can make a feature appear on one developer’s machine before the team has consciously adopted it. If a repository has a genuine need to pin a language version, make it an explicit, documented decision and pair it with SDK version control. For now, the framework-aligned default is clearer and safer.
Nullable analysis is a compiler contract, not a formatting preference
Before nullable reference types, this declaration gave no useful information to the compiler:
public Player FindPlayer(Guid playerId)
Can the method return null when the player does not exist? Is a caller expected to check? The answer is buried in convention, tests, or runtime failures.
With <Nullable>enable</Nullable>, the API can express that difference:
public Player? FindPlayer(Guid playerId)
The compiler now expects callers to account for absence:
var player = repository.FindPlayer(playerId);
if (player is null)
{
return Results.NotFound();
}
return Results.Ok(player);
This setting enables two related forms of feedback:
- Annotations such as
string?, which express whether a reference may be null. - Flow analysis, through which the compiler follows checks such as
if (player is null)and warns when a potentially null value is dereferenced.
A warning is not proof that a null-reference exception will occur. It is a request to resolve an ambiguity in the code’s contract. Sometimes the correction is a null check; sometimes it is changing a return type, requiring a constructor argument, or fixing an incorrect assumption. In the next lesson, you will work through those decisions in an existing code path without suppressing legitimate diagnostics.
For the current repository baseline, enabling nullable analysis immediately is preferable to leaving it until the codebase is much larger. The earlier it is enabled, the smaller the migration surface and the more trustworthy the annotations become.
Built-in analyzers: select a policy before adding tools
The .NET SDK includes analyzers for code quality, reliability, security, performance, design, and style. Their diagnostic IDs commonly begin with prefixes such as:
CAfor .NET code-quality rulesIDEfor editor and code-style rules
AnalysisMode chooses how broadly the SDK enables these analyzer rules. For this project:
<AnalysisMode>Recommended</AnalysisMode>
is a pragmatic starting point. It gives the team a wider, recommended built-in rule set without adopting every rule indiscriminately. An “all rules” policy can be valuable in a mature codebase with the time to assess and configure each rule, but it often produces a large amount of initial noise. Noise encourages blanket suppression, which defeats the point of analysis.
Configure code analysis rules - .NET - Microsoft Learn
Read this Microsoft Learn guide to distinguish analyzer enablement from diagnostic severity. That distinction is essential when you want CI to enforce useful rules without accidentally creating an unmaintainable configuration.
In General options, read the configuration layers, then focus on Analysis mode and Enable code analysis. In Severity level, study the severity levels. Finally, in Precedence, read rule precedence.
The analyzer setting answers: which rule set should be active?
It does not answer: how serious should a particular rule be for this repository?
That second question belongs primarily in .editorconfig.
.editorconfig is the rule-policy layer
Create a root-level .editorconfig alongside Directory.Build.props:
root = true
[*.cs]
dotnet_diagnostic.CA1822.severity = warning
dotnet_diagnostic.IDE0005.severity = warning
This small example does two things:
CA1822reports a member that can be madestatic, when applicable.IDE0005reports unnecessaryusingdirectives.
The exact rules are less important than the mechanism. In an .editorconfig, this pattern sets an individual rule’s severity:
dotnet_diagnostic.RULE_ID.severity = warning
With TreatWarningsAsErrors enabled, a diagnostic configured as warning blocks the build. That may sound indirect, but it is useful: .editorconfig describes the quality policy, while Directory.Build.props describes the build gate.
A typical severity strategy is:
| Severity | Appropriate meaning |
|---|---|
error | This is independently build-breaking and should almost never be violated |
warning | The team expects it to be fixed; the shared build gate can promote it to failure |
suggestion | Helpful IDE guidance, but not a CI requirement |
silent | Supports code cleanup and editor tooling without visible diagnostics |
none | Disabled after an intentional decision |
Prefer an individual rule configuration over blanket suppression. This is clear:
dotnet_diagnostic.CA1822.severity = none
This is risky in a shared build file:
<NoWarn>CA1822;CA2007;CAxxxx</NoWarn>
NoWarn can hide a growing collection of unrelated concerns and gives reviewers little context about why each rule was waived. If a rule is not valuable for the project, document the decision next to a targeted .editorconfig setting. If a single code location requires an exception, use the narrowest available mechanism and explain the reason in the code review.
Also note the precedence model: a rule-specific setting overrides a category-level setting, which overrides a global analyzer setting. This makes it possible to set broad defaults while retaining carefully chosen exceptions.
Should every warning fail the build?
For a new portfolio project, the answer is generally yes, provided the repository begins clean. A warning-free build is a useful operational signal:
- a nullable warning cannot silently become accepted debt;
- analyzer findings are examined when they are introduced;
- local builds and CI apply the same standard;
- a reviewer does not need to decide whether a new warning is “probably fine.”
The important qualifier is that a failure is not a license to silence diagnostics until green. Use this sequence when a warning appears:
- Understand the diagnostic and the code path.
- Fix the underlying design or behavior if it identifies a real issue.
- Adjust the code to communicate the correct contract when the behavior is already valid but unclear to the compiler.
- Configure or suppress the rule narrowly only when it is demonstrably inappropriate for this codebase or location.
For example, do not enable XML documentation generation merely because it creates documentation warnings and then spend time writing filler comments for every public type. XML documentation can be useful for a public NuGet package; it is not automatically a worthwhile quality gate for an internal service. Quality policy should represent a reasoned engineering decision, not the maximum number of checkboxes.
The following short demonstration shows how shared props and .editorconfig cooperate in a solution.
.NET Project Setup From Scratch Using These 6 Best Practices
Watch “.NET Project Setup From Scratch Using These 6 Best Practices” by Milan Jovanović for a concise walkthrough of solution-level .editorconfig placement and centralized build settings.
First watch EditorConfig scope to see how a root .editorconfig affects projects and how nested files can refine settings. Then watch shared props. Focus on the distinction between settings inherited from Directory.Build.props and settings that were previously repeated in an individual project file.
Apply and verify the baseline
Move shared properties out of individual project files after adding the repository-level file. For example, reduce an API project file to project-specific information:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<AssemblyName>GameBackend.Api</AssemblyName>
</PropertyGroup>
</Project>
A test project may still contain test-specific package references and test settings, but it should inherit net10.0, nullable analysis, and the shared analyzer baseline.
Build the solution in Release configuration:
dotnet build GameBackend.sln --configuration Release
Then confirm the inherited properties in at least one API project and one test project using the IDE’s project properties or build output. If a setting appears not to apply, investigate in this order:
- Check that the file is named exactly
Directory.Build.props. - Confirm it is in an ancestor directory of the affected
.csproj. - Look for a closer
Directory.Build.propsfile. - Search the project and imported build files for an overriding property.
- Clean and rebuild after changing MSBuild import files.
Keep the first commit focused:
chore: establish solution build and code-quality baseline
It should include:
Directory.Build.props.editorconfig- simplified project files with duplicated shared settings removed
- any small, justified fixes required for the build to become warning-free
Avoid adding a third-party analyzer package in the same initial commit unless the team has agreed on its purpose and ongoing maintenance. The SDK analyzers already give a strong baseline. A third-party analyzer should be added centrally only when its rules provide specific value, its version is managed deliberately, and its diagnostics are configured with the same discipline as the built-in rules.
Key takeaways
Directory.Build.propssupplies early-imported MSBuild defaults to projects below its directory; place it at the repository root and use the exact filename.- Put shared mechanics there: the .NET target, nullable analysis, analyzer mode, style enforcement, and the warning gate.
Nullableturns nullability from convention into a compiler-checked contract.AnalysisModeenables a rule set;.editorconfigsets the severity and behavior of specific rules.- A warning-as-error policy works when the team fixes causes, makes narrowly documented exceptions, and refuses to create a silent warning backlog.
Next, you will use this newly enabled nullable analysis to eliminate nullability warnings in an existing C# code path without hiding valid diagnostics.
Can't find a good explanation? Sign up and we'll make it for you
Sign up