Evil Monkeypatching in C# with Rosyln Source Generators

I’ve been working on an OSS project recently where I wanted to seamlessly redirect a call that a developer thinks they’re using to do some additional bits. I couldn’t find any real documentation on this, so I thought I’d investigate some ways to do it. Special Thanks to Mark Rendle for helping me with being evil.

Now I have to fully qualify my classes right back to global:: because you’re evil

Mark Rendle

What is Monkeypatching?

If you’re unfamiliar with the term Monkey patching, it’s a process of changing some code “on-the-fly”, where the author of the code didn’t necessarily want that behaviour. It isn’t, however, something you can do in C# (or .NET) in general. There are some libraries like Harmony that can do it partially, but are based around your code running and doing the patching of the libraries. The general premise is that you want to redirect a call between a method the code thought it was going to call, and some other method. This can be incredibly useful if want to change/fix the functionality of something you don’t control. Khalid (@buhakmeh) has a post on some of these approaches here, and we’re going to look at an alternative that has a very narrow use case.

What are Roslyn Source generators?

If you’re not familiar with source generators, they’re a new bit of functionality in .NET that allow you to generate source code at build time that the developer doesn’t see. They can be used in a few different ways, and specifically, they’re really useful for allowing users to use Attributes to generate additional code. However, that’s not what we’re going to do here as we don’t want the developer to have to change anything.

The Code

There is sample code here, and each commit shows the different phases. In this basic example, we’ll redirect our own code that is using System.Console to our new PrefixConsole.  Our new class prepends “WithPrefix: ” to the front of all our console WriteLines.

Phase 1 – We hate ourselves

This is obviously not a real world example of what would someone would do (unless they’re REALLY evil and hate themselves).  What we’re proving here is that we can redirect a call in our code from one type (in this case the `Console` class from the Framework) to another type (PrefixConsole).  We’ll come onto something a little more interesting soon.

First, we’ll create a clean new console app

dotnet new console && dotnet run
Hello World!

Next our new Console Prefixer class, you’ll see if a normal C# class, nothing special.  We’ll then internally call the System.Console class.  The full namespace is important here, otherwise you’ll end up in an infinite loop.

public static class PrefixConsole
{
    public static void WriteLine(string text)
    {
        System.Console.WriteLine("WithPrefix: " + text);
    }
}

Then we’ll add using in our class that will override the usage.

using Console = monkeypatch_test.PrefixConsole;

So, that’s not bad.  We’re pointing our own class at this, so we can see where it’s happening, and it’s pretty obvious where we’re sending the calls.  When we use our IDE to go to the definition, it will take us to our PrefixConsole class, so there’s nothing dodgy here, just a little indirection that we probably didn’t need to do…

Now, lets take this a step further to annoy the rest of our team

Phase 2 – We hate our team mates (Global using)

So now, lets move that using statement as it’s too obvious what we’re doing.  Also, we want ALL the usages of `Console` to be prefixed, and we’re too lazy to go into every class file and do it.  So let’s a global using.

Global using statements came in with C# 10.  They allow you to really cut down on the bloat in our files.  If you’ve used Razor templates before, you’ll have done something similar where you added all the using states to View_start.cshtml.

We’ll add this `Globals.cs` but the name doesn’t matter.  Then remove our using statement from the file.

global using Console = monkeypatch_test.PrefixConsole;

This will point all references to `Console` (that aren’t fully qualified) to our new PrefixConsole.

So, that’s getting bad now.  It’s not obvious from looking at the code of your class that you’re being redirected.  At least, however, you’re IDE will navigate you to the write class.

So, now, let’s make things even worse.

Phase 3 – We hate everyone (Source generators)

In the previous 2 phases, we’ve been using our class in our project.  That’s annoying, but not that bad.  It’s useful if you want to provide some consistency, and even if that class is in a NuGet package, it’s not terrible.

If it’s in a NuGet package though, people need to add a pesky line of code to their solution to do that redirection.  That’s pretty bad, why would we want people who use our library to write MORE lines of code?

So lets use a source generator to do that redirection, that will REALLY mess people up and makes us popular everywhere.

First, lets move that class into our new console library called EvilConsolePrefixer (because this is where we get a little evil).  We can also remove our Globals.cs too now.

We can then add the SourceGenerator packages to our new library.

dotnet add package Microsoft.CodeAnalysis.Analyzers
dotnet add package Microsoft.CodeAnalysis.CSharp.Workspaces

Now we can add the really evil part.  We’re going to tell the generator to add our Global.cs to the main project at compile time, without the developer knowing.

[Generator]
public class EvilConsolePrefixerGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        context.AddSource("Globals", "global using Console = EvilConsolePrefixer.PrefixConsole;");
    }
    public void Initialize(GeneratorInitializationContext context)
    {
    }
}

This will add a source file into the compile pipeline with a single line that will do our redirection.  Why do it this way, and not just add a global using in thing the EvilConsolePrefixer class? global usings (and usings in general) are scoped to the project, so they wouldn’t transition into the calling project. Using a SourceGenerator like this means that our global using will be added to the main project as if it was code that the developer wrote.

All that remains is to add the EvilConsolePrefixer project as a reference to our main project.  As we’re doing this locally (i.e. not through NuGet), we’ll need to add an additional attribute to the import.  This isn’t required if we use a NuGet package.

  <ItemGroup>
    <ProjectReference Include="..\EvilConsolePrefixer\EvilConsolePrefixer.csproj" OutputItemType="Analyzer"/>
  </ItemGroup>

The additional attribute is OutputItemType="Analyzer"

What makes this evil you might ask?

  1. Mark Rendle said it’s Evil and I shouldn’t do it.
  2. We shouldn’t redirect calls, it makes code hard to reason about as your context isn’t correct.
  3. Redirecting at compile time like that could break your user’s compilation as your new code may not have all the methods and properties that the original class did.
  4. IDE’s will navigate the user to the original class, not the one being injected at compile time
  5. Decompiling the solution will look like the developers directly referenced your code, which isn’t actually correct.

So why would you do this Martin?

I came across this as I was trying to find a way to add a shim onto a sealed class from the Microsoft BCL.  The goal was to provide a package that allowed people using that class to get a wrapper very easy without having to change their code.

Unfortunately, extension methods can’t override normal methods, and because this class (ActivitySource) doesn’t come from any kind of factory, and was sealed I couldn’t inherit and override.

In addition, what I wanted to use in my new code was [CallerLineNumber] and [CallerFilePath], which need to be done at build time as it’s the only time it is available (without PDBs).  You couldn’t, for instance, use an existing interface without those properties, and simply add them to your class that implements the interface.

This solution works for that use case, and they’ll be blog post about the library I’m creating soon, despite it being evil, it serves a very real benefit.  I just hope that this isn’t misused, and removed at some point in the future.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Website Built with WordPress.com.

Up ↑

%d bloggers like this: