Sharing coding style and Roslyn analyzers across projects

  • Gérald Barré

I've written many times about enforcing a coding style and enabling static analysis in a project:

What if you now want to share a coding style and Roslyn analyzers across multiple projects in a company?

Recent version of .NET and Roslyn have made it possible to share a coding style using .NET packages. Let's see how it works.

#NuGet packages and MSBuild imports

The .NET SDK imports .props and .targets files from NuGet packages. For instance, if the package is named MyPackage, the SDK imports build/MyPackage.props and build/MyPackage.targets. So, you can add any MSBuild properties and items to the project that references the package.

For instance, you can include common project configuration properties in the package.

MyPackage.props (MSBuild project file)
<Project>
  <PropertyGroup>
    <ReportAnalyzer>true</ReportAnalyzer>
    <Features>strict</Features>
    <Deterministic>true</Deterministic>
    <NoWarn>$(NoWarn);CA1014</NoWarn>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <AnalysisMode>All</AnalysisMode>
    <AnalysisLevel>preview</AnalysisLevel>
    <TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
    <EnforceCodeStyleInBuild Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</EnforceCodeStyleInBuild>
  </PropertyGroup>
</Project>

You can also reference a global editorconfig file using the EditorConfigFiles item.

MyPackage.props (MSBuild project file)
<ItemGroup>
  <EditorConfigFiles Include="global.editorconfig" />
</ItemGroup>

Finally, you can reference Roslyn analyzers by using package dependencies in the NuSpec files.

MyPackage.nuspec (nuspec)
    <dependencies>
      <dependency id="Meziantou.Analyzer" version="1.0.700" />
      <dependency id="Microsoft.VisualStudio.Threading.Analyzers" version="17.1.46" />
      <dependency id="Microsoft.CodeAnalysis.BannedApiAnalyzers" version="3.3.3" />
    </dependencies>

#Building the package

Folder structure:

/MyPackage.nuspec
/src/build/MyPackage.props
/src/build/MyPackage.targets
/src/files/NamingConvention.editorconfig
/src/files/Analyzers.editorconfig
/src/files/BannedSymbols.txt

Let's start with the nuspec file:

MyPackage.nuspec (nuspec)
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <!-- Package metadata -->
    <id>MyPackage</id>
    <developmentDependency>true</developmentDependency>
    <description>A package to configure .NET coding style and static analysis</description>

    <!-- The version is set when building the package -->
    <version>$version$</version>

    <!-- Add analyzers -->
    <dependencies>
      <dependency id="Meziantou.Analyzer" version="1.0.700" />
      <dependency id="Microsoft.VisualStudio.Threading.Analyzers" version="17.1.46" />
      <dependency id="Microsoft.CodeAnalysis.BannedApiAnalyzers" version="3.3.3" />
    </dependencies>
  </metadata>

  <!-- Add props / targets / editorconfig files to the package -->
  <files>
    <file src="src\**" target="" />
  </files>
</package>

You can configure the project using the props / target files. The following files are examples, you can use them as a starting point.

src/build/MyPackage.props (MSBuild project file)
<Project>
  <PropertyGroup>
    <ReportAnalyzer>true</ReportAnalyzer>
    <Features>strict</Features>
    <Deterministic>true</Deterministic>
    <NoWarn>$(NoWarn);CA1014</NoWarn>
    <EnableNETAnalyzers>true</EnableNETAnalyzers>
    <AnalysisMode>All</AnalysisMode>
    <AnalysisLevel>preview</AnalysisLevel>
    <TreatWarningsAsErrors Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</TreatWarningsAsErrors>
    <EnforceCodeStyleInBuild Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</EnforceCodeStyleInBuild>
  </PropertyGroup>
</Project>
src/build/MyPackage.targets (MSBuild project file)
<Project>
  <!-- Register the editorconfig files to the project -->
  <ItemGroup>
    <EditorConfigFiles Include="$(MSBuildThisFileDirectory)\..\files\*.editorconfig" />
  </ItemGroup>

  <!-- Banned Symbols -->
  <PropertyGroup>
    <IncludeDefaultBannedSymbols Condition="$(IncludeDefaultBannedSymbols) == ''">true</IncludeDefaultBannedSymbols>
  </PropertyGroup>

  <ItemGroup>
    <AdditionalFiles Include="$(MSBuildThisFileDirectory)\..\files\BannedSymbols.txt"
                     Condition="$(IncludeDefaultBannedSymbols) == 'true'"
                     Visible="false" />
  </ItemGroup>
</Project>

You can configure the project using global editorconfig file. You can create as many editorconfig files as you want. For instance, you can create a file for the naming convention, and another file to configure the Roslyn analyzers.

src/files/NamingConvention.editorconfig
is_global = true
global_level = -1

# name all constant fields using PascalCase
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols  = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style    = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds   = field
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_naming_style.pascal_case_style.capitalization = pascal_case

# ... other naming rules

Configure the Roslyn analyzers using a global editorconfig file:

Analyzers.editorconfig
is_global = true
global_level = 0

dotnet_code_quality_unused_parameters=all:warning
dotnet_diagnostic.IDE0059.severity = warning
csharp_style_unused_value_assignment_preference = discard_variable

dotnet_diagnostic.CA1063.severity = none
dotnet_diagnostic.CA1303.severity = none
dotnet_diagnostic.CA1305.severity = none
dotnet_diagnostic.CA1816.severity = none
dotnet_diagnostic.MA0004.severity = warning

# ... other rules

As the project use Microsoft.CodeAnalysis.BannedApiAnalyzers, let's create a default banned symbols file:

BannedSymbols.txt
P:System.DateTime.Now;Use System.DateTime.UtcNow instead
P:System.DateTimeOffset.Now;Use System.DateTimeOffset.UtcNow instead
M:System.IO.File.GetCreationTime(System.String);Use GetCreationTimeUtc instead

Finally, you can build the package using the nuget pack command. You can download nuget.exe on nuget.org.

PowerShell
nuget pack MyPackage.nuspec -ForceEnglishOutput -Version 1.0.0

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub