.NET Notebook Update - Distribution of a Common EditorConfig File to Multiple .NET Projects

The .NET Notebook has been updated to answer the frequently-asked question about how to distribute a common or centralised EditorConfig file, with Code Style and Analysis Rules, to multiple .NET Projects or Solutions without duplication.

See the new notebook page Distribution of a Common EditorConfig File to Multiple .NET Projects, or the full .NET Notebook.

Distribution of a Common EditorConfig File to Multiple .NET Projects

Overview

This information answers the frequently-asked question about how to distribute a common or centralised EditorConfig file, with Code Style and Analysis Rules, to multiple .NET Projects or Solutions without duplication.

Background

In the past, Code Analysis and Code Style could be enforced in your .NET code bases through the usage of Rule Set files.

These Rule Set files supported the concept of including parent Rule Set files (to allow different mixtures of rules to be created more easily), and they could be included easily in and distributed through your own NuGet package to all of your .NET Projects.

During 2020 however, and starting in Visual Studio 2019 version 16.5, Rule Set files were deprecated in favour of the EditorConfig file.

EditorConfig

EditorConfig helps to maintain consistent coding styles across various Text Editors and IDEs. EditorConfig is supported in many tools, including Visual Studio 2019, Visual Studio Code, and many other editors.

These configuration files work differently from Rule Set files, in that:

  1. they are meant only for IDEs or Text Editors; and
  2. they do not support the concept of including parent EditorConfig files.

EditorConfig files (named .editorconfig) are intended to reside within the folder structure of your source code. It relies upon the hierarchical nature of the file system, where configuration in EditorConfig files in a child folder take precedence over those in parent or ancestor folders.

Within an EditorConfig file, the severity of a rule is specified with the syntax:

dotnet_diagnostic.<rule ID>.severity = <severity>

The valid values for severity are:

  • error
  • warning
  • suggestion
  • silent
  • none
  • default

Limitations

There are a number of limitations with regards to the use of EditorConfig files in the .NET world.

Unfortunately, EditorConfig files are not a one-for-one functional replacement of Rule Set files. For example, rules that apply globally, or have Location.None cannot be specified in an EditorConfig file. One of these rules is CA1014: Mark assemblies with CLSCompliantAttribute. Currently this NoWarn workaround is required for CA1014 and similar.

Given that EditorConfig files are intended for Text Editing tools, and their rules are defined and applied in a hierarchical fashion within the source code folder structure, EditorConfig files cannot be referenced from within the contents or file location of a NuGet Package.

Global AnalyzerConfig Files to the Rescue (But Not Quite Yet!)

To address the limitations of EditorConfig, Microsoft introduced the concept of a Global AnalyzerConfig, starting from Visual Studio 2019 16.8 and the .NET 5 SDK.

These files provide options that apply to all the source files in a .NET Project, regardless of their file names or file paths - which means that AnalyzerConfig files can be specified in and distributed through NuGet packages.

In a MSBuild project file, they are specified:

  • with the GlobalAnalyzerConfigFiles property within an ItemGroup property; or
  • the file simply can be named .globalconfig and automatically included for use.

Unlike EditorConfig files, Global AnalyzerConfig files cannot be used to configure editor style settings for IDEs, such as indent size or whether to trim trailing whitespace. Instead, they are designed purely for specifying project-level analyser configuration options.

AnalyzerConfig files are used only during the compilation of the source code, and are not used in the Text Editors or IDEs. This immediately makes one concerned about the duplication of rules that overlap between the editor and those for compilation.

The rules specified in EditorConfig files in the source hierarchy take precedence over those specified in the AnalyzerConfig files.

Current State of Play

So here is the current state of play (as of January 2021):

  1. EditorConfig files cannot be referenced from the folder structure of a NuGet package;
  2. AnalyzerConfig files can be referenced from the folder structure of a NuGet package; and
  3. EditorConfig file settings take precedence over AnalyzerConfig file settings.

All of this sounds great until one realises that:

  1. If there are conflicting entries in two global AnalyzerConfig files, a compiler warning is reported and both entries are ignored; and
  2. It is desirable to set the AnalysisMode to AllEnabledByDefault so that all rules are automatically enabled by default as build warnings, and from there you opt to change the severity of individual rules.

However, if one chooses any AnalysisMode other than Default, the severity rules provided within the .NET SDK are specified, and if you have your own Global AnalyzerConfig file, the rules will likely conflict and therefore be ignored!

This is the situation (as of January 2021) - until Microsoft releases an update to the AnalyzerConfig mechanism to allow a global_level setting to be specified to allow for a hierarchy of rule precedence among AnalyzerConfig files.

Until then, if you are like me and you want to use <AnalysisMode>AllEnabledByDefault</AnalysisMode>, then your only option to override some of those default settings individually is within an EditorConfig file.

And so now we come full circle - back to asking the question of how we can distribute an EditorConfig file via a NuGet package.

Approach

I searched far and wide on the Internet for how to distribute a common EditorConfig file via a NuGet package so that it could be used by a team in multiple projects and solutions. In the end, all of the answers I saw said the same thing - you cannot, and that is not the intended purpose of such a file as the source code folder location is critical.

An EditorConfig file cannot be used or referenced from within the contents of a NuGet package.

After a little creative thought however, I realised that it is possible to distribute an EditorConfig file to a .NET Project via a NuGet package. Note the subtle difference - distribution versus referencing the file.

My approach is simply to:

  • include a common EditorConfig file within the contents of the NuGet package; and
  • hook into the MSBuild BeforeBuild Target in order to copy the EditorConfig file from the NuGet package’s contents into the .NET Project’s folder structure (if it has changed).

Thus, when a .NET Project is built for the first time (and subsequently), the EditorConfig file will always exist in the source code hierarchy. Thereafter, the file exists for:

  • when the Code Analysis mechanism executes; and
  • the Text Editor or IDE to perform live analysis at design time.

Caveats and Assumptions

  • Given the EditorConfig file is always copied to the Project folder, the developer cannot make persistent changes to that file - but:

    (a) they could add their own EditorConfig file to the parent folder of the Project folder; or

    (b) you or your team could change the destination location specified in your NuGet package to instead copy the EditorConfig file to the parent of the Project folder, and thus allow your developers to override settings just for that Project. This may be useful to allow the specification of project-specific rules or source code rule suppressions, but depending on how you or your team uses this power, it may defeat some of the general purpose of having a central or common standard rule file.

  • One should add the EditorConfig file that is copied to the Project file to your .gitignore file so it is not committed in source control and duplicated that way.

Solution

Show me the code!

I have created template for some NuGet EditorConfig distribution libraries on GitHub in my ChannelAdam.CSharp.Snippets repository.

Below is an explanation of how it works.

Don’t forget to customise the rules in the EditorConfig file for the purposes of your team/project! The rules I prefer may not be the rules you prefer!

NuGet MSBuild .props File

The following information relates to my example .props file distributed within the NuGet package.

A NuGet package can include a MSBuild .props file of the same name that can contain Properties, Package References and Targets that execute when a build is performed and are applied to the project that references the NuGet package.

In this .props file, I specify:

  • Properties to enable Code Analysis;
  • Properties to specify some desired rules;
  • A Target to copy the EditorConfig file to the Project; and
  • Package references for Code Analysis tools.

Properties to Enable Code Analysis

Here are the properties I set in the .props file.

 1<PropertyGroup>
 2  <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
 3  <EnableNETAnalyzers>true</EnableNETAnalyzers>
 4  <AnalysisMode>AllEnabledByDefault</AnalysisMode>
 5  <AnalysisLevel>latest</AnalysisLevel>
 6  <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
 7  <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
 8  <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
 9  <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
10  <GenerateDocumentationFile>true</GenerateDocumentationFile>
11  <Nullable>enable</Nullable>
12  <WarningsAsErrors>nullable</WarningsAsErrors>
13  <LangVersion>latest</LangVersion>
14</PropertyGroup>

Of note:

  • EnforceCodeStyleInBuild is a new feature that allows code style rules specified in the EditorConfig file to be checked as part of the build;
  • EnableNETAnalyzers is true by default for .NET 5.0, but I am setting it to true to support earlier versions of .NET;
  • AnalysisMode of AllEnabledByDefault turns on all the rules that ship with the .NET SDK - set to the severity level of warning
  • AnalysisLevel of latest so that the latest set of rules for your .NET SDK will be applied;
  • RunAnalyzersDuringBuild and RunAnalyzersDuringLiveAnalysis controls when the analyser executes;
  • I do not want warnings to be treated as errors, because I presumably have meticulously and consciously determined and customised the rules in my EditorConfig file to distinguish between rules that must be adhered to, versus rules that really should be attended to and resolved but do not matter as much.
  • Nullable is enabled because the newish Nullable Reference Types feature is fantastic and all developers should think more about appropriately handling null and avoiding costly null reference issues in production;
  • WarningsAsErrors set to nullable is short-hand for listing a number of CS86?? nullable rules to be treated as errors (because we want to enforce the avoidance of obvious potential null reference issues); and
  • LangVersion of latest allows us to use the latest C# language features and syntax goodness - as long as we don’t try to use features that won’t compile - e.g. using the .NET 5.0 record type from .NET Core code.

Properties to Specify Desired Rules

As mentioned earlier, rule CA1014 has a Location.None and cannot be specified in an EditorConfig file. And since this approach currently cannot use Global AnalyzerConfig files, I need to disable this rule in the .props file.

 1<PropertyGroup>
 2  <!-- 
 3    Workaround for changing severity of rules with Location.None - e.g. CA1014
 4      - see https://github.com/dotnet/roslyn/issues/37876#issuecomment-738042719
 5    Needed until those types of rules can be disabled in a Global Analyzer Config 
 6      - when https://github.com/dotnet/roslyn/issues/48634 is implemented.
 7    # CA1014: Mark assemblies with CLSCompliantAttribute
 8  -->
 9  <NoWarn>$(NoWarn);CA1014</NoWarn>
10</PropertyGroup>

Target to Copy the EditorConfig File to the Project

First we need to specify the location of the EditorConfig file to copy from within the NuGet package folder structure.

1<ItemGroup>
2  <EditorConfigFilesToCopy Include="$(MSBuildThisFileDirectory)..\content\Rules\.editorconfig" />
3</ItemGroup>

Then we can use the MSBuild Copy Task to copy the file to the folder of the .NET Project that is being built. This Target is defined to execute before the MSBuild BeforeBuild Target.

1<Target Name="CopyEditorConfig" BeforeTargets="BeforeBuild">
2    <Message Text="Copying the .editorconfig file from '@(EditorConfigFilesToCopy)' to '$(MSBuildProjectDirectory)'"></Message>
3    <Copy 
4      SourceFiles="@(EditorConfigFilesToCopy)"
5      DestinationFolder="$(MSBuildProjectDirectory)"
6      SkipUnchangedFiles="true"
7      UseHardlinksIfPossible="false" />
8</Target>

Package References for Code Analysis Tools

The new Microsoft.CodeAnalysis.NetAnalyzers NuGet package and also the Roslynator.Analyzers packages that will perform the actual Code Analysis also are specified in the .props file.

BTW, I’m a huge fan of the Roslynator extensions for both Visual Studio Code and Visual Studio - and you should be too!

 1<ItemGroup>
 2  <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
 3    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 4    <PrivateAssets>all</PrivateAssets>
 5  </PackageReference>
 6  <PackageReference Include="Roslynator.Analyzers" Version="3.0.0">
 7    <PrivateAssets>none</PrivateAssets>
 8  </PackageReference>
 9  <PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="1.0.0">
10    <PrivateAssets>all</PrivateAssets>
11    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
12  </PackageReference>
13  <PackageReference Include="Roslynator.Formatting.Analyzers" Version="1.0.0">
14    <PrivateAssets>all</PrivateAssets>
15    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
16  </PackageReference>
17</ItemGroup>

NuGet Project File

The following information relates to my example .csproj file for the NuGet package.

Packing

When packing, NuGet by default ignores files that begin with a period.

The following property overcomes that and allows our .editorconfig file to be packed.

1<PropertyGroup>
2  <NoDefaultExcludes>true</NoDefaultExcludes>
3</PropertyGroup>

The following configuration specifies:

  • the .props file in the build folder should be packed; and
  • the .editorconfig file in the Rules folder should be packed.
1<!-- Select the MSBuild props and composed .editorconfig files to pack -->
2<ItemGroup>
3  <None Include="build\My.EditorConfig.NuGet.Package.props" Pack="true" PackagePath="build\" />
4  <Content Include="Rules\.editorconfig" Pack="true" PackagePath="content\Rules"></Content>
5</ItemGroup>

Bonus Points

As indicated earlier, Rule Sets can have different base Rule Sets to allow multiple types of Rule Set files to be mixed depending on the need.

A similar thing can be done by composing different EditorConfig files in different NuGet packages.

I have allowed for this by creating a common Template folder that contains a common base file for the EditorConfig (editorconfig.base) and I compose the final Rules\.editorconfig file through file concatenation that takes place in an MSBuild Target.

 1<!-- Compose the .editorconfig file from the templates -->
 2<Target Name="CreateEditorConfig" BeforeTargets="BeforeBuild">
 3  <ItemGroup>
 4    <EditorConfigFilesToJoin Include="
 5        ..\My.EditorConfig.NuGet.Package.Internal\Templates\editorconfig.base;
 6        ..\My.EditorConfig.NuGet.Package.Internal\Templates\require-configureawait.rules" />
 7  </ItemGroup>
 8  <ItemGroup>
 9    <EditorConfigFileContents Include="$([System.IO.File]::ReadAllText(%(EditorConfigFilesToJoin.Identity)))"/>
10  </ItemGroup>
11  <WriteLinesToFile File="Rules\.editorconfig" Lines="@(EditorConfigFileContents)" Overwrite="true" />
12</Target>

Similarly, the .props file itself is shared (as is) between all the NuGet packages, so I have stored it in a common build folder and have a Target to copy it as the NuGet package is being created.

1<!-- Copy the common .props file -->
2<Target Name="CopyPropsFile" BeforeTargets="BeforeBuild">
3    <Copy 
4      SourceFiles="..\My.EditorConfig.NuGet.Package.Internal\build\My.EditorConfig.NuGet.Package.Internal.props"
5      DestinationFiles="build\My.EditorConfig.NuGet.Package.props"
6      SkipUnchangedFiles="true"
7      UseHardlinksIfPossible="false" />
8</Target>

Conclusion

This approach is relatively simple to implement (with a template like I have provided), very simple to use (just by installing you own NuGet package), and achieves the purpose of distributing a common EditorConfig file to multiple .NET Projects and Solutions in order help developers in a team achieve consistently styled code with your own tailored standards.