Skip to content

Tenacom/PolyKit

Repository files navigation

PolyKit - C# polyfills, your way

License Latest release Latest stable release Changelog

Build, test, and pack CodeQL

Repobeats analytics image

Latest packages NuGet MyGet
PolyKit PolyKit @ NuGet PolyKit @ MyGet
PolyKit.Embedded PolyKit.Embedded @ NuGet PolyKit.Embedded @ MyGet


Read this first

Hi there! I'm Riccardo a.k.a. @rdeago, founder of Tenacom and author of PolyKit.

I won't insult your intelligence by explaining what a polyfill, a package reference, or a MSBuild property is. If these are new concepts to you, well... we both know you have a browser and aren't afraid to use it. 😉

Throughout this document, I'll assume that you know what follows:

  • what is a polyfill and when you need polyfills;
  • how to create a C# project;
  • the meaning of TargetFramework (or TargetFrameworks) in the context of a .csproj file;
  • the meaning of PackageReference in the context of a .csproj file;
  • the meaning of PrivateAssets="all" in a PackageReference;
  • how to make minor modifications to a project file, for example adding a property or an item.

If you're yawning, great, read on. If you're panicking, do your googling due diligence and come back. I'll wait, no problem.

What is PolyKit

PolyKit is both a run-time library (provided via the PolyKit NuGet package) and a set of ready-to-compile source files (provided via the PolyKit.Embedded NuGet package) that add support for latest C# features as well as recent additions to the .NET runtime library, even in projects targeting .NET Framework or .NET Standard 2.0.

How you use PolyKit depends on your project:

  • for a single-project application or library, such as a simple console program or a source generator DLL, internal polyfills provided by the PolyKit.Embedded package will suit just fine;
  • for a library whose public-facing APIs may require polyfills, for example if some public method accepts or returns Spans, public polyfills provided by the PolyKit package will polyfill dependent applications where necessary, and get out of the way on platforms that do not require them;
  • for a multi-project solution, with some application and several shared libraries, you may want to avoid code duplication by using the public polyfills provided by the PolyKit package;
  • if your solution contains a "core" library, referenced by all other projects, you may even spare an additional dependency by incorporating public polyfills in your own library.

How it works

PolyKit employs the same technique used by the .NET Standard library to expose public types on older frameworks without creating conflicts on newer ones: types that need no polyfilling (for example the StringSyntax attribute on .NET 5+) are forwarded to their .NET runtime implementation.

This way you can safely reference PolyKit and use, even expose, polyfilled types in a .NET Standard 2.0 library, because the actual type used will depend upon the target framework of each application.

The same goes for public polyfills created by PolyKit.Embedded. In this case, however, to ensure that no type conflicts happen at runtime, you should multi-target according to the application target frameworks you want to support. For example, if your library targets .NET Standard 2.0 only, a .NET 6.0 application will "see" two identical StringSyntax types: one in the .NET runtime and the other in PolyKit.dll.

The solution is simple: when using PolyKit.Embedded to add public polyfills to a library, set your TargetFrameworks property so that you generate all possible sets of polyfills.

The optimal set of target frameworks for public polyfills is equal to the target frameworks of the PolyKit package. At the time of writing it is net462;net47;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0, but you may refer to this NuGet page at any time to find out which frameworks are targeted by the latest version of PolyKit.

Compatibility

.NET SDK

PolyKit and PolyKit.Embedded require that you build dependent projects with .NET SDK version 6.0 or later.

Target frameworks

Polyfills provided by PolyKit and PolyKit.Embedded are compatible with all flavors of .NET supported by Microsoft at the time of publishing, as well as all corresponding versions of .NET Standard:

  • .NET Framework 4.6.2 or greater;
  • .NET 6 or greater;
  • .NET Standard 2.0 or greater.

Build toolchain

A minimum of Visual Studio / Build Tools 2022 and/or .NET SDK 6.0 is required to compile the polyfills provided by PolyKit.Embedded.

Language version

C# language version 10.0 or greater is required to compile the polyfills provided by PolyKit.Embedded.

It is recommended to set the LangVersion property to latest in projects that reference PolyKit or PolyKit.Embedded, in order to take advantage of all the polyfilled features.

Analyzers, code coverage, and other tools

All code provided by PolyKit.Embedded is well-behaved guest code:

  • all source files bear the "auto-generated" mark, so that code style analyzers will happily skip them;
  • every source file name ends in .g (e.g. Index.g.cs) so that it can be automatically excluded from code coverage;
  • all added types are marked with a GeneratedCode attribute;
  • all added classes and structs are marked with ExcludeFromCodeCoverage and DebuggerNonUserCode attributes.

This ensures that using PolyKit.Embedded will have zero impact on your coverage measurements, code metrics, analyzer diagnostic output, and debugging experience.

Features

PolyKit provides support for the following features across all compatible target frameworks.
Please note that some types will only be polyfilled when compiling with a .NET SDK version that actually supports them.

Feature Minimum .NET SDK version Notes
nullable reference types, including null state analysis 6.0
indices and ranges 6.0 Note #1
init accessor on properties and indexers 6.0
DateOnly struct 6.0 Note #2
TimeOnly struct 6.0 Note #2
HashCode struct 6.0 Note #3
caller argument expressions 6.0
AsyncMethodBuilder attribute 6.0
ModuleInitializer attribute 6.0
SkipLocalsInit attribute 6.0
UnconditionalSuppressMessage attribute 6.0
RequiresPreviewFeatures attribute 6.0
UnmanagedCallersOnly attribute 6.0
ValidatedNotNull attribute 6.0 Note #4
StackTraceHidden attribute 6.0 Note #5
support for writing custom string interpolation handlers 6.0
Enumerable.TryGetNonEnumeratedCount<TSource> method 6.0 Note #6
ISpanFormattable interface 6.0 Note #7
trimming incompatibility attributes 6.0 / 7.0 Note #8
required members 7.0
scoped modifier (including the UnscopedRef attribute) 7.0
CompilerFeatureRequired attribute 7.0
ConstantExpected attribute 7.0
StringSyntax attribute 7.0
Experimental attribute 8.0

Note #1: This feature depends on System.ValueTuple, which is not present in .NET Framework versions prior to 4.7. If you reference the PolyKit.Embedded package in a project targeting .NET Framework 4.6.2, you must also add a package reference to System.ValueTuple; otherwise, compilation will not fail, but features dependent on ValueTuple will not be present in the compiled assembly.

Note #2: Polyfills for DateOnly and TimeOnly, unlike their .NET Runtime counterparts, do not support the IParsable and ISpanParsable<TSelf> interfaces because they contain static virtual members, a feature of C# 11.0 that cannot be polyfilled.
The methods are there, you can use them (for instance you can call DateOnly.Parse(str)), but they are just public methods of the individual types, not associated with any interface.

Note #3: In projects referencing PolyKit.Embedded and targeting .NET Framework or .NET Standard 2.0, method HashCode.AddBytes(ReadOnlySpan<byte>) will only be compiled if package System.Memory is also referenced.

Note #4: This is not, strictly speaking, a polyfill, but it spares you the trouble of defining your own internal ValidatedNotNullAttribute or referencing Visual Studio SDK.
The attribute provided by PolyKit is in the PolyKit.Diagnostics.CodeAnalysis namespace.

Note #5: Polyfilling StackTraceHiddenAttribute would be worthless without providing actual support for it. PolyKit cannot replace relevant code (Exception.StackTrace getter and StackTrace.ToString() method) in frameworks prior to .NET 6.0.
PolyKit provides two extension methods, Exception.GetStackTraceHidingFrames() and StackTrace.ToStringHidingFrames(); these methods retrofit the behavior of Exception.StackTrace and StackTrace.ToString() respectively to frameworks prior to .NET 6, where StackTraceHiddenAttribute was first introduced.
When used on .NET 6.0+, the above extension methods are just façades for their BCL counterparts. Be aware of the following limitations:

  • the output of the "retrofitted" extension methods is always in US English, regardless of any locale setting;
  • external exception stack trace boundaries ("End of stack trace from previous location" lines) are missing from returned strings.

Note #6: Obviously PolyKit cannot extend System.Linq.Enumerable, so we'll have to meet halfway on this.
PolyKit adds a System.Linq.PolyKitEnumerable class, containing a TryGetCountWithoutEnumerating<TSource> method that will just call TryGetNonEnumeratedCount<TSource>on .NET 6.0+ and replicate most of its functionality (except where it requires access to runtime internal types) on older frameworks.

Note #7: PolyKit does not (and can not) add ISpanFormattable support to .NET Runtime types: intVar is ISpanFormattable will still be false except on .NET 6.0 and later versions.
You can, however, implement ISpanFormattable in a type exposed by a multi-target library, no #if needed: it will behave just as you expect on .NET 6.0+, and still have a TryFormat method on older platforms (unless you used an explicit implementation).
Also note that, in projects referencing PolyKit.Embedded and targeting .NET Framework or .NET Standard 2.0, ISpanFormattable will only be compiled if package System.Memory is also referenced.

Note #8: RequiresDynamicCodeAttribute was introduced with .NET 7.0 and requires .NET SDK 7.0 to be properly supported duting build.

Quick start

How to use shared polyfills across multiple projects

  • Ensure that all your target frameworks are supported by PolyKit.
  • Add a package reference to PolyKit to your projects.

How to add polyfills to a stand-alone project (simple application, source generator)

  • Ensure that all your target frameworks are supported by PolyKit.
  • Set the LangVersion property in your project to Latest, Preview, or at least 10.
  • Add a package reference to PolyKit.Embedded to your project.
    Remember to set PrivateAssets="all" if you add the package reference manually.
  • Add optional package references as needed (see notes #1 and #2 above).

How to create your own shared polyfill library

  • Ensure that all your target frameworks are supported by PolyKit.
  • Set the LangVersion property in your project to Latest, Preview, or at least 10.
  • Add a package reference to PolyKit.Embedded to your library project.
    Remember to set PrivateAssets="all" if you add the package reference manually.
  • Add optional package references as needed (see notes #1 and #2 above).
  • Set the PolyKit_GeneratePublicTypes property to true in your project file.
  • Add your own code to the library.
  • You can now use your own library instead of PolyKit in your projects.

Contributing

Of course we accept contributions! 😃 Just take a look at our Code of Conduct and Contributors guide, create your fork, and let's party! 🎉

Contributors


Riccardo De Agostini
Add your contributions