Microsoft .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.

.NET 5.0 and Guidance for Library Developers

Overview

The release of .NET 5.0 in November 2020 marked a significant milestone for the .NET ecosystem.

Developers of reusable libraries especially need to understand:

  • the change in approach to unify .NET Standard and .NET Core; and
  • the new .NET 5.0 Target Framework Monikers (TFM).

Background

In December 2014, Microsoft introduced .NET Core as a foundation for open source and cross-platform support, with the goal of .NET having a single code base.

In September 2016, Microsoft introduced the .NET Standard as a set of APIs that all .NET platforms have to implement.

In May 2019, Microsoft provided clarity on the future of .NET that:

  • new applications should be built on .NET Core 3.0+; and
  • .NET Framework 4.8 will be the last major version of .NET Framework (to be supported as long as Windows itself is supported).

At the same time, Microsoft introduced the vision for .NET 5 as the combination of “.NET Core and the best of Mono to create a single platform that you can use for all your modern .NET code”.

Unification of .NET Standard and .NET Core

Retrospectively, the approach of the .NET Standard has been very successfully to provide uniformity within the .NET ecosystem. It also has suffered from a number of issues, including the delay between specification changes and the implementation of those changes in the individual platform code bases.

.NET 5.0 adopts a different approach to establishing uniformity within the .NET ecosystem.

.NET 5.0 is the beginning of a single cross-platform .NET runtime and framework implementation that unifies .NET Core and the .NET Standard. It provides the same runtime behaviours and developer experiences across all platforms with a set of APIs, languages, and tools that target a broad set of application types, including: mobile, cloud, desktop, and IoT.

Microsoft will NOT be releasing a new version of .NET Standard. .NET 5 and all future versions will continue to support .NET Standard 2.1 and earlier, and should be considered the foundation for all new code.

.NET 5.0 Target Framework Monikers (TFM)

net5.0 is the new Target Framework Moniker (TFM) for .NET 5.0. It combines and replaces the netcoreapp and netstandard TFMs that are specified in project files.

1<Project Sdk="Microsoft.NET.Sdk">
2
3  <PropertyGroup>
4    <TargetFramework>net5.0</TargetFramework>
5  </PropertyGroup>
6
7</Project>

Multiple frameworks can be targeted, using <TargetFrameworks> as per the following example.

1<Project Sdk="Microsoft.NET.Sdk">
2
3  <PropertyGroup>
4    <TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
5  </PropertyGroup>
6
7</Project>

net5.0-windows will be used to expose Windows-specific functionality, including Windows Forms, WPF and WinRT APIs.

.NET 6.0 will use the same approach, with net6.0, and will add net6.0-ios and net6.0-android.

See the Microsoft documentation for Operating System specific TFMs.

Guidance on Targeting .NET 5.0 vs .NET Standard

Microsoft have updated their documentation to provide guidance on when to target net5.0 vs. netstandard.

The only reason to re-target from .NET Standard to .NET 5.0 would be to gain access to more runtime features, language features, or APIs.

For example, in order to use C# 9, you need to target .NET 5.0.

You can multi-target .NET 5.0 and .NET Standard to get access to newer features and still have your library available to other .NET implementations.

Most widely used libraries will end up multi-targeting for both .NET Standard 2.0 and .NET 5.

Supporting .NET Standard 2.0 gives you the most reach, while supporting .NET 5 ensures you can leverage the latest platform features for customers that are already on .NET 5.

TFM Usage Cheat Sheet

Here is the Cheat Sheet for guidance on TFM usage:

  • do NOT target .NET Standard 1.x for new libraries (.NET Core 1.x is end-of-life and is no longer supported);
  • target a minimum of netstandard2.0 if you want to support minimum usage of .NET Core 2.x or .NET Framework (from 4.6.1). See .NET Standard Versions for more information on version compatibility;
  • target a minimum of netstandard2.1 if you want to support minimum usage of .NET Core 3.x or share code between Mono, Xamarin, and .NET Core 3.x; and
  • target a minimum of net5.0 if you do NOT need to support the .NET Core 2.x, .NET Core 3.x or .NET Framework (from 4.6.1).

Boiling it all down to one sentence: in order to enable the largest reach of your library, multi-target both netstandard2.0 and net5.0.

Extra Guidance on Targeting .NET Standard 1.x

If you have an existing library that already targets .NET Standard 1.x:

  • keep targeting .NET Standard 1.x in order to maintain compatibility with existing usage of your library;
  • understand that .NET Standard 1.x is distributed as a granular set of NuGet packages, which creates a large package dependency graph and results in developers downloading a lot of packages when building, so seriously consider additionally multi-targeting with netstandard2.0 and net5.0 to provide a better experience for developers using later versions of .NET.

Tips for Upgrading to .NET Standard 2.0

.NET Version Overview

  • .NET Standard 2.0 is a specification for a set of cross-platform APIs that all platform implementations of that standard must support.
  • .NET Core 2.0 is a cross-platform implementation of that .NET Standard 2.0
  • .NET Framework is a Windows-only implementation of various .NET APIs
  • .NET Framework 4.7.1 fully implements .NET Standard 2.0

System.* Packages Dead-Ended from .NET Core 2.0

Referencing .NET Standard 2.0 Libraries

  • Full .NET Framework 4.7.1 class libraries can reference any .NET Standard 2.0 (or earlier) library
  • However, a .NET Standard 2.0 library cannot necessarily reference and use a full .NET Framework class library - because that library may no comply with the .NET Standard.
  • It is estimated that at the time of the release of .NET Core 2.0, approximately 70% of existing NuGet packages can be used without an issue - BUT you won’t know there is an issue without thorough testing with that library!
  • Microsoft has decided to emit a nice ugly compiler warning and warning signal against a reference in the Solution Explorer when this is the case.
  • Recommendation: Use a full .NET Framework class library if you have references to any non .NET Standard class libraries - to avoid the compiler warning and ensure that the library does not have compatibility issues.
  • .NET Framework 4.6.1 with the .NET Core 2.0 SDK installed also supports .NET Standard 2.0 - but due to the way that the libraries are packaged, this can cause assembly binding problems - so better to target 4.7.1 from the start.
    • For example:
      • When running under .NET Core 2, the Tuple type is in assembly: System.ValueTuple, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e.
      • When running under .NET Framework 4.6.1, the Tuple type is in: System.ValueTuple, System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51.
      • But, when starting with a .NET Framework 4.6.1 application that uses a .NET Standard 2.0 library, the .NET Standard 2.0 code wants: System.ValueTuple, Version=0.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
    • If your application targeted 4.6.1 and .NET Framework 4.7.1 was later installed, any referenced .NET Standard libraries may experience System.TypeLoadException exceptions. The workaround being to re-target your application to 4.7.1.

.NET Framework Libraries & Automatic Assembly Binding Redirects

  • By default, assembly binding redirects aren’t added to full .NET Framework class library projects.
  • This is problematic for unit testing projects as they are essentially like applications
  • Add the following into the .csproj:
    1<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    2<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
    
  • And then remove all the manual binding redirects from the app.config file
  • See also: How to enable and disable automatic binding redirection

Compiler warnings

  • With .NET Framework libraries referencing .NET Standard 2.0 libraries, Microsoft.Extensions.DependencyInjection.Abstractions causes a compiler warning:
    • “Resolved file has a bad image, no metadata, or is otherwise inaccessible. The system cannot find the path specified. (Exception from HRESULT: 0x80070003)”
    • This should be fixed in a future .NET SDK update: Issue 1544 and Issue 1548