Hello everyone, I am the Wolf at the End of the Desert.
This article was first published on Dotnet9, introducing the use of the Lib.Harmony library to intercept methods in third-party .NET libraries, enabling modification of method logic and expected behavior without altering their source code. Moreover, interception is not limited to classes and methods with public access modifiers. The article is organized as follows:
- What is method interception?
- Intercepting a sample program
- How to intercept non-public methods?
- Summary
1. What is method interception?
Method interception refers to inserting custom code before or after a method is called to modify its behavior. Through method interception, developers can validate input parameters, modify return values, log method calls, and perform other operations without changing the original code.
This article uses the Lib.Harmony library to intercept methods in third-party libraries. The site owner has previously written an article titled Learn this skill - .NET API interception technique, which you may want to review. However, that article did not cover how to intercept non-public classes and methods, and this article will supplement that.
2. Intercepting a sample program
2.1. Writing a program to retrieve number paragraphs
Create a .NET class library project, for example named TestDll, and add a utility class TestTool:
namespace TestDll;
public class TestTool
{
/// <summary>
/// Beautiful paragraphs with numbers
/// </summary>
private readonly List<string> _sentences = new()
{
"One is the symbol of loneliness, the spokesperson of solitude, standing alone at the beginning of a verse, inspiring reverie.",
"Two is the existence of opposites, companions in duality, shadowing each other, interdependent.",
"Three is the perfect number, the stability of a triangle, bringing harmonious rhythm to poetry.",
"Four is the symbol of balance, the cycle of seasons, making the structure of poetry more solid.",
"Five is the vibrant number, colorful blossoms, blooming into beautiful pictures in poetry.",
"Six is the ordinary number, the shape of a hexagon, bringing a sense of stability to poetry.",
"Seven is the mysterious number, the rainbow of seven colors, radiating magical light in poetry.",
"Eight is the infinite number, the universe of eight directions, extending the imagination of poetry endlessly.",
"Nine is the perfect number, the winding rivers, bringing a flowing beauty to poetry.",
"Ten is the complete number, the symbol of perfection, making the ending of poetry more perfect."
};
/// <summary>
/// Get the paragraph corresponding to a number
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
public string GetNumberSentence(int number)
{
var mo = number % _sentences.Count;
// If units digit is 0, take the last one
if (mo == 0)
{
mo = 10;
}
if (mo == 6)
{
mo = 1;
}
var sentencesIndex = mo - 1;
return _sentences[sentencesIndex];
}
}
The logic of the method GetNumberSentence above: takes an integer parameter number, takes modulus 10 (the count of the _sentences collection), and returns the paragraph for numbers within 10. If the modulus is 6, it returns the paragraph for number 1 (this is added to verify the interception logic).
Below is a test UI written with AvaloniaUI. The UI is not the focus of this article, so we directly show the GIF and code screenshots. The source code link is also provided at the end:


2.2. Why does the number 6 always show the paragraph for number 1?
Analyzing the code above, we want to remove the logic where mo == 6 sets mo = 1. Besides using decompilation tools like dnSpy to modify the code, we can also use Lib.Harmony (Learn this skill - .NET API interception technique - Dotnet9) to intercept the GetNumberSentence method.
- Install the
Lib.Harmonypackage
<PackageReference Include="Lib.Harmony" Version="2.3.0-prerelease.2" />
- Write an interception replacement class
Refer to Learn this skill - .NET API interception technique - Dotnet9 and add the following interception replacement class:
- Register the original class type, original method name, and parameter data types on the interception class
- Copy the original method code into the interception replacement method
Prefix, and obtain the properties and fields of the original class via reflection (e.g., the_sentencescollection) - Comment out the code for
mo == 6
using HarmonyLib;
using System.Reflection;
using TestDll;
namespace MultiVersionLibrary;
/// <summary>
/// HarmonyPatch attribute associates the intercepted class and method
/// </summary>
[HarmonyPatch(typeof(TestTool))]
[HarmonyPatch(nameof(TestTool.GetNumberSentence))]
[HarmonyPatch(new Type[] { typeof(int) })]
internal class HookGetNumberSentence
{
/// <summary>
/// GetNumberSentence interception replacement method
/// </summary>
/// <param name="__instance">Intercepted TestTool instance</param>
/// <param name="number">Same parameter name as in GetNumberSentence, modify it to tamper with the method parameter</param>
/// <param name="__result">Return value of GetNumberSentence, modify it to forge the method result</param>
/// <returns></returns>
public static bool Prefix(ref object __instance, int number, ref string __result)
{
try
{
// Copy the entire original method logic, then make partial modifications
//1. _sentences is a private field of the intercepted class TestTool; we get its value via reflection
var sentences =
__instance.GetType().GetField("_sentences", BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(__instance) as List<string>;
if (sentences?.Any() != true)
{
__result = "Ah, no beautiful sentences?";
return true;
}
var mo = number % sentences.Count;
// If units digit is 0, take the last one
if (mo == 0)
{
mo = 10;
}
// 2. Comment out the code we consider ambiguous
//if (mo == 6)
//{
// mo = 1;
//}
var sentencesIndex = mo - 1;
__result = sentences[sentencesIndex];
return false;
}
catch (Exception ex)
{
return true;
}
}
}
Don't forget to register the automatic interception class method in the Program or App.xaml initialization method:
var harmony = new Harmony("https://dotnet9.com");
harmony.PatchAll(Assembly.GetExecutingAssembly());
Re-run the main program, and when you input the number 6, the paragraph corresponding to number 6 is displayed correctly:

This achieves the result of tampering without modifying the third-party library's source code. The site owner encountered an exception when using .NET 8 for interception and switched to .NET 6 to run normally. The exception information is as follows; it may be that Lib.Harmony does not yet support .NET 8:
HarmonyLib.HarmonyException:“Patching exception in method System.String TestDll.TestTool::GetNumberSentence(System.Int32 number)”
TypeInitializationException: The type initializer for 'MonoMod.Utils.DMDEmitDynamicMethodGenerator' threw an exception.
InvalidOperationException: Cannot find returnType fieeld on DynamicMethod
3. How to intercept non-public methods?
3.1. Modify the number paragraph retrieval method
Again modify the TestTool class and add the GetNumberSentence2 method, adding a number validation operation mo = new CalNumber().GetValidNumber(mo); to the method. The method definition is as follows:
/// <summary>
/// Get the paragraph corresponding to a number
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
public string GetNumberSentence2(int number)
{
var mo = number % _sentences.Count;
// If units digit is 0, take the last one
if (mo == 0)
{
mo = 10;
}
// New number validation method
mo = new CalNumber().GetValidNumber(mo);
var sentencesIndex = mo - 1;
return _sentences[sentencesIndex];
}
The validation method is defined as follows:
- The
CalNumberclass andGetValidNumbermethod are declared withinternal, meaning they are only usable within the current project.
internal class CalNumber
{
internal int GetValidNumber(int number)
{
// Some complex algorithm code can be added here
if (number == 6)
{
number = 1;
}
return number;
}
}
And change the main project's call to the number retrieval paragraph method to:
public string? Number
{
get { return _number; }
set
{
_number = value;
TryParse(_number, out var factNumber);
// Switch to method 2
Message = _testTool.GetNumberSentence2(factNumber);
}
}
When inputting 6, it returns the paragraph for 1 again:

The question arises: how to intercept an internal method?
We do not directly comment out the code mo = new CalNumber().GetValidNumber(mo);. What if the validation method is very important? We only need to modify part of its logic; the overall original logic should remain unchanged.
3.2. How to intercept internal methods?
Add an interception class HookGetValidNumber. Now we can no longer add attributes on the class ([HarmonyPatch(typeof(CalNumber))]) because CalNumber does not have a public access modifier and cannot be directly used across projects; the syntax does not support it:

If attributes cannot be used, we manually register the method to be intercepted. This is the focus of this article. The code is below; a brief explanation:
- The manual registration code is similar to automatic registration using attributes, just written differently;
- The interception replacement method needs to be wrapped using the
HarmonyMethodmethod; harmony.Patch(hookMethod, replaceHarmonyMethod);associates the intercepted method with the replacement method.
/// <summary>
/// Manually register the association between the intercepted method and the replacement method
/// </summary>
public static void StartHook()
{
var harmony = new Harmony("https://dotnet9.com");
var hookClassType = typeof(TestTool).Assembly.GetType("TestDll.CalNumber");
var hookMethod = hookClassType!.GetMethod("GetValidNumber", BindingFlags.NonPublic | BindingFlags.Instance,
new[] { typeof(int) });
var replaceMethod = typeof(HookGetValidNumber).GetMethod(nameof(Prefix));
var replaceHarmonyMethod = new HarmonyMethod(replaceMethod);
harmony.Patch(hookMethod, replaceHarmonyMethod);
}
The replacement method is defined as follows:
- The method name
Prefixis not restricted here, as long as it matches the one manually registered (var replaceMethod = typeof(HookGetValidNumber).GetMethod(nameof(Prefix));). - If the number equals 6, modify the forged result to 8.
/// <summary>
/// GetNumberSentence interception replacement method
/// </summary>
/// <param name="__instance">Intercepted TestTool instance</param>
/// <param name="number">Same parameter name as in GetNumberSentence, modify it to tamper with the method parameter</param>
/// <param name="__result">Return value of GetNumberSentence, modify it to forge the method result</param>
/// <returns></returns>
public static bool Prefix(ref object __instance, int number, ref int __result)
{
// Copy the entire original method logic, then make partial modifications
// Some complex algorithm code can be added here
if (number == 6)
{
number = 8;
}
__result = number;
return false;
}
Finally, add one line of manual registration code after the original automatic registration code, and that's it:
// 1. Automatic registration of interception classes: add intercepted class and method attributes on the interception class
var harmony = new Harmony("https://dotnet9.com");
harmony.PatchAll(Assembly.GetExecutingAssembly());
// 2. Manual registration of interception classes, construct intercepted class and method information for interception
HookGetValidNumber.StartHook();
The running effect is as follows: entering 6 displays the paragraph for number 8:

4. Summary
- For technical communication or to join the group, please add wechat ID: codewf
- Sample code in the article: MultiVersionLibrary
The uses of the two registration methods for interception with the Lib.Harmony library are as follows:
- Automatic registration:
- By using attributes on the interception class to associate the intercepted class and method definitions, you can achieve automatic registration of interception logic. This approach is suitable when there are many classes and methods to be intercepted, reducing manual registration workload and improving development efficiency.
- Automatic registration usually only associates with public classes or methods because the IDE filters and prompts based on code visibility.
- Manual registration:
- By constructing the intercepted class and method definitions through code for manual registration, you can have more flexible control over the interception logic. This approach is suitable for scenarios that require customized interception logic, allowing selection of specific classes and methods to intercept and fine-tuning the interception configuration.
- Manual registration is more flexible and can intercept various classes and methods, including internal ones. Manual registration can achieve the association of non-public classes and methods by writing code, but it should be noted that this may increase code complexity and maintenance costs.