Contributing to .NET

This post is about my recent experience of contributing to .NET, which has been open source since late 2014. I've nosed around the github repo several times as I'd always intended to contribute back to the project. So, after just 7 short years, I made the effort and contributed a change!

In this post, I'll describe how to find something to work on, the steps to get and build the runtime (on Windows), and how to contribute a change to a library.

There were some surprises during this journey, including the discovery that Linq usage is being reduced in the framework!

How to find something to work on #

The first step is actually finding something to work on. In the GitHub repo, there are 'starter tasks' that are labelled up-for-grabs. These tasks can be things like bug fixes, adding unit tests, performance fixes, or documentation. As well as up-for-grabs, there are also tasks for implementing new APIs. These are labelled 'api-approved'. When searching, you can combine labels; here's the list of api-approved and up-for-grabs tasks.

The GitHub Issue that I decided to do relates to config binding. I'll describe it a bit more later on, but basically, I chose this one as it seemed easy enough to fix and looked like a nice gentle introduction to the repo and the associated processes and workflows.

If you do want to contribute, then this is a great starting page, and the workflow guide is a good reference page.

For additional help, or to chat, there's a Discord 'server'. It has many channels, such as general, newbies, Android, maui, etc. I found the users very knowledgable and happy to help.

Building the runtime #

The runtime is on GitHub at Create your own fork of it. You can do that at the top right of the page. This creates a copy of the repository in your GitHub account. Once you've done that, check it out to your computer.

You'll notice after a day or so, that the branches of your fork will fall behind the dotnet repo. You can check how far behind your copy is on your github page. You'll also be able to fetch and merge from the upstream branches:

You can do the same on the command line by adding a 'remote' to the dotnet/runtime repo:
git remote add upstream

After doing that, you'll see the dotnet/runtime upstream. To verify, use git remote -v:

You can now sync the upstream repo with your forked repo (cmd):

git pull upstream main & git push origin main

or, PowerShell:

git pull upstream main ; git push origin main

Back to the contents of the repo; there are 3 sub-components:

Runtime and CoreLib must both be compiled using the same configuration, e.g. Debug/Release. Unless you're working in those areas, it's recommended to compile these as Release.
Libraries can be built with Debug. They still run fast enough and can be debugged easily.

Among the solutions in the repo, are:

To configure your Windows environment to build the runtime, refer to the requirements guide (which is summarised below):

Once you've installed and configured that lot, you're ready to build!

In the root, run:


and go for a cup of tea!

During the build - over the noise of the PC fans! - I noticed the anti-malware process was using a lot of CPU, as was Tortoise Git:

I've seen this in past and read that the solution is to add exclusions in Microsoft Defender. Exclusions are things like folders and processes that Windows Defender should ignore.
Here's mine:

The fix this time was to add additional excludes for cmake.exe and ninja.exe.

The fix for Tortoise was:

TortoiseGit -> Settings -> Icon Overlays => choose "Shell" or "None" to disable

Once those processes had calmed down a bit, the build took about 25 minutes.

The output is written to the artifacts folder, e.g.

In that folder will be things like mscorlib.dll, System.Collections.dll, etc.

How to contribute #

After you've chosen an up-for-grabs task, you should leave a comment on the issue saying that you'd like to look at the issue. An 'area owner' will then assign it to you.
Next, create a branch in your fork of the repo. Name the branch something like issue-1234.

The issue that I picked up related to config binding. I'll summarise the issue here so that you don't have to read through the whole thing: basically, there was a request to make config binding throw an exception when a field specified in config doesn't exist on the object being bound to.

So, given this config:

"config": {
"data": [ "a", "b" ],
"foo": "whatever",

and this POCO:

public class Config {
public List<string> Data { get; set; }

... an exception should be thrown when binding:

var ret = config.GetSection("config")
.Get<Config>(o => o.ErrorOnUnknownConfiguration = true);

The ErrorOnUnknownConfiguration is a new entry on BinderOptions.

I won't go into detail on the actual fix (you can see how it was done on the Pull Request), but I will describe some of the more surprising and unusual things I came across.

Since I chose a library to work on, I followed the the Building Libraries document. In short, this document has a 'typical daily workflow' with some command-line instructions. I modified these slightly for a PowerShell prompt:

# From root:
git clean -xdf
git pull upstream main ; git push origin main

# Build Debug libraries on top of Release runtime:
.\build.cmd clr+libs -rc Release
# Performing the above is usually only needed once in a day, or when you pull down significant new changes.

# If you use Visual Studio, you might open System.Text.RegularExpressions.sln here.
.\build.cmd -vs System.Text.RegularExpressions

# OR - if not using Visual Studio, then use the rest of this script...

# Switch to working on a given library (RegularExpressions in this case)
cd src\libraries\System.Text.RegularExpressions

# Change to test directory
cd tests

# Then inner loop build / test
pushd ..\src ; dotnet build ; popd ; dotnet build /t:test

I spent a fair amount of time digging around the project structure and figuring out the best place to implement the feature. Whilst digging around, the first thing that surprised me was the duplicate projects under the ref folder:

These are called Reference Assemblies. They are projects that mirror a real project; they have the same name, namespaces, public types, and public methods, but, they have empty implementations of the methods. From the documentation on Reference Assemblies:

Reference assemblies are a special type of assembly that contain only the minimum amount of metadata required to represent the library's public API surface. They include declarations for all members that are significant when referencing an assembly in build tools, but exclude all member implementations and declarations of private members that have no observable impact on their API contract. In contrast, regular assemblies are called implementation assemblies.

Reference Assemblies are used in the runtime to target multiple platforms (emphasis mine):

A reference assembly can also represent a contract, that is, a set of APIs that don't correspond to the concrete implementation assembly. Such reference assemblies, called the contract assembly, can be used to target multiple platforms that support the same set of APIs. For example, .NET Standard provides the contract assembly, netstandard.dll, that represents the set of common APIs shared between different .NET platforms. The implementations of these APIs are contained in different assemblies on different platforms, such as mscorlib.dll on .NET Framework or System.Private.CoreLib.dll on .NET Core. A library that targets .NET Standard can run on all platforms that support .NET Standard.

These reference assemblies need to be updated if you change the API of an assembly. Fortunately, an MSBuild target is supplied that does that:

dotnet msbuild /t:GenerateReferenceAssemblySource

More information on this can be found here. I did observe a bug here: it stripped a lot of attributes from the reference assembly. Thankfully this was picked up in the PR.

Because the feature I chose involved adding to a public enum, it meant an API review was required. Thankfully, this had already been done. I did want to throw a custom exception in the implementation of the feature, but that would've required another API review, so I was advised to use an existing exception.

One other thing that I found surprising was the suggestion in the PR to replace my use of Linq. I was slightly amazed and upset at this!

But it seems there's a valid reason. The runtime team are trying to reduce the use of Linq in critical paths to reduce startup time and memory use. Linq can cause overhead in terms of speed and memory, for example, there's overhead in invoking delegates, and there's GC pressure creating iterators for the clauses in Linq queries. Most of the time though, Linq is off the 'hot path' (i.e. away from tight loops). I can understand the desire for the runtime team to want to eek out every bit of performance as that would benefit a huge amount of apps built on it. Here's some more information on the drive to remove Linq.

So, after adding the implementation for the request and some unit tests, I wanted to have a test console app to see new feature working in a real app. I initially tried to add this test project to the solution in the runtime repo, but that caused a fair few build issues. I was advised to create a project outside of the repo and directly reference the DLLs I just built in the library. That worked: the feature worked as expected, and after a few rounds of implementing suggestions, the PR was approved and merged to main!

My work is done for another 7 years!

But really, I do think I'll carry on contributing. For me, aside from the satisfaction of contributing back, it was interesting to see how such a large repo is organised and managed.


Since you've made it this far, sharing this article on your favorite social media network would be highly appreciated 💖! For feedback, please ping me on Twitter.

Leave a comment

Comments are moderated, so there may be a short delays before you see it.

1 comments on this page

  • Lee


    Java rocks