Many of us are dependent on open-source software (OSS). Countless hours of work, passion, and effort go into delivering even the smallest packages, and often these critical dependencies are handled by single individuals. Even the most meticulously planned and thought out packages are bound to include bugs.

The presence of bugs in dependencies is nothing new, and we’ve all experienced our share of production crashing exceptions. What is different is our ability to recognize the issue in OSS code and contribute a solution.

In general, being a good consumer of OSS is about giving back to the creators in reproducible exceptions, issues, documentation, and code fixes. In a perfect scenario, everyone involved has time to discuss, understand, and ultimately address the issue. We all know nothing is ever perfect, and life can get in the way of fixing and publishing fixes.

Let’s look at the issue and how monkey patching might be the solution to help ease some of the tension in .NET OSS.

The “Broken” Dependency

Folks on both sides of the OSS equation can grow frustrated that a dependency isn’t working the way it should. We’ll assume that the consumer of a dependency has done their due diligence, identified the bug, and submitted a patch to the author of the OSS project. The consumer is left waiting for the author to merge and publish a new package with the fix. In an ideal scenario, this entire interaction would take hours. Sadly, many PRs are left unmerged and critical packages long abandoned by authors.

The .NET community has behaviors that aim to “solve” the issues that arise with dependencies.

Republished Packages

Since most authors publish their source code under a permissive license like MIT, the community can rebuild and publish packages under similar names. One example of this is the PagedList implementation found on NuGet. While duplicating packages can solve an immediate issue for the forking party, it creates fragmentation within the community. If the original author decides to revive the original project, there are now two community efforts working on a similar codebase.

The fragmentation also adds a compounded negative effect, where dependent packages can build on top of functionality but need to choose which dependent package. In the end, folks are diffusing the value that could they could deliver to the community.

The Built-In Dependency

Publishing and maintaining packages are time-consuming for the owner. A standard solution for many is to pull the source code into their solution for an undetermined amount of time. The consumer, having access to all the source code, can fix the issue and compile the application.

The consumer gets past the initial issue but now has to sync between the originating source and their solution. Additionally, dependencies may be non-trivial codebases and take significant effort to incorporate and build alongside an existing solution. Finally, the consumer has isolated the fix to their solution, with the community determining their workarounds independently.

Get Microsoft To Do It

If a dependency becomes critical enough, the community might ask Microsoft to build it directly into the .NET base class library. Getting Microsoft to fix something isn’t a viable short-term fix, as most folks are running into a critical exception at the time and don’t have the luxury of waiting for an entity like Microsoft to take an interest in fixing a dependency.

Having Microsoft take over any critical dependency will drive less innovation in our space in the long term. Ultimately, Microsoft is a business, and businesses have to justify expending resources to shareholders. The projects that Microsoft will take over have to have a pathway to making them money. A community package to generate digital pets is not likely to be saved by Microsoft unless those digital pets run in Azure.

I would also argue that having a mono source of innovation and dependency creation is unhealthy for all ecosystems, including the .NET space.

All these approaches have their issues, but what if we could explore another method that helped propel folks toward a solution while not damaging the .NET OSS community?

What Is Monkey Patching?

Monkey patching is commonly associated with dynamic programming languages like Ruby and Python.

The term monkey patch only refers to dynamic modifications of a class or module at runtime, motivated by the intent to patch existing third-party code as a workaround to a bug or feature which does not act as desired. Wikipedia

Applications for monkey patching include:

  • Replacing methods, classes, attributes, and functions at runtime.
  • Modify third-party dependencies without maintaining a private copy of source code.
  • Distribute essential security fixes.

Like all powerful concepts, developers utilizing monkey patching can use it for nefarious reasons. Modifying existing code at runtime could expose security tokens and allow access to sensitive information. Additionally, folks may use monkey patching to disable licensing checks and steal access to otherwise commercial software. Monkey patching can also destabilize software, causing exceptions or catastrophic breakdowns in logic. Saying monkey patching is a concept to be used with extreme caution would be an understatement.

Great, let’s see how we can use it!

Monkey Patching a .NET Dependency

In the previous section, we mentioned that monkey patching is commonly associated with dynamic languages. That doesn’t mean we can’t do monkey patching in .NET; we absolutely can!

There are libraries that allow .NET developers to monkey patch dependencies are Harmony and Pose. For this article, let’s focus on a Harmony example. We first need to install the NuGet package. I found Harmony to be more capable and would recommend it..

dotnet add package Lib.Harmony

Let’s look at a broken dependency for a Calculator class.

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using HarmonyLib;

class Program {
    static void Main(string[] args) {
        var code = new Calculator();
        
        // should write 2
        Console.WriteLine(code.Add(1, 1));
    }
}

public class Calculator {
    public int Add(int x, int y)
    {
        // this is broken
        return x - y;
    }
}

Yikes! The Add method has faulty logic. It is subtracting our variables instead of adding them together. Let’s patch our Calculator class with the correct behavior.

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using HarmonyLib;

class Program {
    static void Main(string[] args) {
        var code = new Calculator();
        
        // should write 2
        Console.WriteLine(code.Add(1, 1));
    }
}

public class Calculator {
    public int Add(int x, int y)
    {
        // this is broken
        return x - y;
    }
}

[HarmonyPatch(typeof(Calculator), nameof(Calculator.Add))]
class Patch {
    static bool Prefix(ref int __result, int x, int y) {
        __result = x + y;
        // skip the original
        return false;
    }
}

public static class Patcher {
    [ModuleInitializer]
    public static void Patch() {
        var harmony = new Harmony("khalid.abuhakmeh");
        var assembly = Assembly.GetExecutingAssembly();
        harmony.PatchAll(assembly);
    }
}

When we run our application, we see that the result is 2, just like we expected! Awesome!

Let’s look at the Patch class in more detail. Harmony matches parameters based on names. In our case, Harmony will pass x and y parameters to our patch method. We also are given a __result parameter, allowing us to set a return value. Finally, we return false, informing Harmony to bypass the original method entirely.

[HarmonyPatch(typeof(Calculator), nameof(Calculator.Add))]
class Patch {
    static bool Prefix(ref int __result, int x, int y) {
        __result = x + y;
        // skip the original
        return false;
    }
}

To apply runtime patching, we need to use the Harmony class and apply our patch on a specific Assembly. In our case, the target is the executing assembly. We can also run our Patch as the first method called in our application using C# 9’s ModuleInitializer attribute. The string khalid.abuhakmeh is an identifier and can be any arbitrary value.

public static class Patcher {
    [ModuleInitializer]
    public static void Patch() {
        var harmony = new Harmony("khalid.abuhakmeh");
        var assembly = Assembly.GetExecutingAssembly();
        harmony.PatchAll(assembly);
    }
}

The Harmony library has many options in altering existing assemblies, and I highly recommend reading the documentation to understand how and when you can modify a dependency.

How Does This Help OSS?

The frustration and burn out of OSS comes from the constant push and pull of forces. A dependency has a system-crashing bug that doesn’t get resolved fast enough. The urgent fire-at-hand can bring out the worst in all of us. Our frustration can bleed over into verbal and abusive behavior that creates toxic situations. As mentioned above, there are workarounds, but applying them have long-term consequences on the community and the viability of an OSS ecosystem.

By monkey patching an existing dependency and submitting a PR to the original author, we do several things:

  1. We fix our problem, moving forward with our day without the stress of waiting for a savior.
  2. We help the OSS author without the pressure of needing an immediate release this minute. They can review, change, and release on their schedule.
  3. Providing a monkey patch that others can apply can help avoid the inevitable “is this project dead?” issues that flood many OSS repositories. The point of OSS to collaborate and gain value from our collective efforts.

Conclusion

.NET is a strongly-typed compiled stack, and generally, we’re stuck with the dependencies we have. When our apps crash or don’t work because of a bug, it can be harrowing. Support for monkey patching should be a first-class feature, as it helps us solve our problems without adding pressure on OSS maintainers or software vendors. Even if there is a potential for misuse, monkey patching benefits greatly outweigh its nefarious uses.

Additionally, it keeps the community from reinventing and republishing otherwise viable projects because of a single bug. Until .NET supports monkey patching, libraries like Harmony are a great alternative to solving our problems and bringing the sense of urgency down in critical situations.

What are your thoughts on monkey patching? Do you think it should be part of the .NET stack by default, or do you think it is too powerful to put in the hands of developers?

Please leave a comment below and let me know what you think. Also, follow me on Twitter @buhakmeh.