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:
- they are meant only for IDEs or Text Editors; and
- 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 anItemGroup
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):
- EditorConfig files cannot be referenced from the folder structure of a NuGet package;
- AnalyzerConfig files can be referenced from the folder structure of a NuGet package; and
- EditorConfig file settings take precedence over AnalyzerConfig file settings.
All of this sounds great until one realises that:
- If there are conflicting entries in two global AnalyzerConfig files, a compiler warning is reported and both entries are ignored; and
- 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
ofAllEnabledByDefault
turns on all the rules that ship with the .NET SDK - set to the severity level ofwarning
AnalysisLevel
oflatest
so that the latest set of rules for your .NET SDK will be applied;RunAnalyzersDuringBuild
andRunAnalyzersDuringLiveAnalysis
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 tonullable
is short-hand for listing a number ofCS86??
nullable rules to be treated as errors (because we want to enforce the avoidance of obvious potential null reference issues); andLangVersion
oflatest
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.0record
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 thebuild
folder should be packed; and - the
.editorconfig
file in theRules
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.