Table of Contents

.NET tips and best-practices

As a .NET project grows, it can be tricky to manage it. There are a few improvements we can do to improve the maintenance. These sections are optional and they don't affect to the build system.

Shared project metadata

One interesting option in MSBuild (builder of .NET projects) is to share properties across .csproj files. If you have a file Directory.Build.props in a root folder, every .csproj projects in subfolders will share its content.

Let's create a file src/Directory.Build.props and fill some common info about our project:

<Project>
  <PropertyGroup>
    <Product>PRODUCT_NAME</Product>
    <Authors>AUTHOR</Authors>
    <Company>COMPANY</Company>
    <Copyright>Copyright (C) YEAR AUTHOR</Copyright>
  </PropertyGroup>

  <!-- NuGet package common info -->
  <PropertyGroup>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageProjectUrl>https://github.com/pleonex/PleOps.Cake</PackageProjectUrl>
    <RepositoryUrl>https://github.com/pleonex/PleOps.Cake</RepositoryUrl>
    <PackageIcon>icon.png</PackageIcon>
    <PackageTags>net;csharp;devops;pipeline;github-actions</PackageTags>
    <PackageReadmeFile>README.md</PackageReadmeFile>
  </PropertyGroup>

  <!-- Code analyzers -->
  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <AnalysisLevel>latest</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All"/>
    <PackageReference Include="SonarAnalyzer.CSharp" PrivateAssets="All"/>
    <PackageReference Include="Roslynator.Analyzers" PrivateAssets="All"/>
  </ItemGroup>
</Project>

Centralize dependency management

An option for .NET projects is to configure centralized NuGet packages. Instead of defining the version of the PackageReferences in each .csproj, we will define all of them in a common file. This will help that all your project use the same version of a dependency.

Create a file src/Directory.Packages.props and fill it with your dependencies. For instance:

<Project>
  <ItemGroup>
    <PackageVersion Include="nunit" Version="3.14.0" />
  </ItemGroup>
</Project>

Then in each .csproj, remove the Version attribute in PackageReference.

You can still override the version of a dependency in one .csproj by using VersionOverride attribute.

To enable this feature, create/open a file src/Directory.Build.props and set ManagePackageVersionsCentrally to true inside a PropertyGroup.

An interesting option for open-source libraries is to enable source link. It allows to inspect the source code while debugging another project. This can also be interested for internal libraries inside a company project.

SourceLink references are enabled by default in .NET 8, but we define a set of additional properties to generate a NuGet (a few kB bigger) that it's compatible with more NuGet feeds (like Azure DevOps).

It's also recommended to enable deterministic builds, which will generate an identical build output if the code doesn't change. In practice this means the binary won't have timestamps. It will help to troubleshoot issues in the future.

Set the following properties in src/Directory.Builds.props:

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
  <!-- Publish the repository URL in the nuget metadata for SourceLink -->
  <PublishRepositoryUrl>true</PublishRepositoryUrl>

  <!-- Embed auto-generated code for SourceLink -->
  <EmbedUntrackedSources>true</EmbedUntrackedSources>

  <!-- For SourceLink and debugging support we don't publish a symbol NuGet
        as some NuGet feeds like Azure DevOps does not provide a symbol server.
        Instead we embed the metadata (PDB) inside the DLLs and EXEs.
        We use this approach instead of providing the .pdb inside the NuGet
        as the latter has known issues with Visual Studio:
        https://github.com/dotnet/sourcelink/issues/628 -->
  <DebugType>embedded</DebugType>

  <!-- Enable deterministic builds -->
  <Deterministic>true</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
Important

If you are not using .NET 8.0 (yet), add a PackageReference (inside an ItemGroup) to your provider as explained in the sourcelink README.

README and icon

NuGet packages for libraries and dotnet tools supports providing a README file and an icon. It will show in Visual Studio and your feed explorer (nuget.org) when searching for the package.

To enable the feature, set PackageIcon and PackageReadmeFile. In the previous Directory.Build.props template, we already set it for every project.

Then in each public library / dotnet tool .csproj add:

<ItemGroup>
  <None Include="../../docs/images/logo_128.png" Pack="true" PackagePath="$(PackageIcon)" Visible="false" />
  <None Include="../../README.md" Pack="true" PackagePath="$(PackageReadmeFile)" />
</ItemGroup>