Hello everyone, I am a wolf at the end of the desert.
本文首发于Dotnet9,介绍使用Lib.Harmony库拦截第三方.NET库方法,达到不修改其源码并能实现修改方法逻辑、预期行为的效果,并且不限于只拦截public访问修饰的类及方法,行文目录:
- What is method interception?
- Example program interception
- How to intercept non-public methods?
- summary
1. What is method interception?
Method interception refers to modifying the behavior of a method by inserting custom code before or after the method is called. Through method interception, developers can verify the input parameters of methods, modify the return value of methods, and record the call log of methods without modifying the original code.
本文使用Lib.Harmony库实现第三方库方法的拦截,关于该库站长写过快学会这个技能-.NET API拦截技法一文,大家可以再看看,但该篇文章未介绍非public类及方法如何拦截,本文会有所补充反过来 。
2. Example program interception
2.1. Write a program to retrieve digital paragraphs
创建一个.NET类库工程,比如叫TestDll,添加工具类TestTool:
namespace TestDll;
public class TestTool
{
/// <summary>
/// 带数字的优美段落
/// </summary>
private readonly List<string> _sentences = new()
{
"一,是孤独的象征,寂寞的代言人, 它独自站在诗句的起点,引人遐想。",
"二,是相对的存在,对立的伴侣, 它们如影随形,互相依存。",
"三,是完美的数字,三角的稳定, 它给诗歌带来了和谐的节奏。",
"四,是平衡的象征,四季的轮回, 它让诗歌的结构更加坚实。",
"五,是生机勃勃的数字,五彩斑斓的花朵, 它们在诗歌中绽放出美丽的画面。 ",
"六,是平凡的数字,六边形的形状, 它们给诗歌带来了一种稳定的感觉。",
"七,是神秘的数字,七色的虹霓, 它们在诗歌中散发出神奇的光芒。",
"八,是无限的数字,八方的宇宙, 它们让诗歌的想象力无限延伸。",
"九,是完美的数字,九曲的江河, 它们给诗歌带来了一种流动的美感。 ",
"十,是圆满的数字,十全十美的象征, 它们让诗歌的结尾更加完美。"
};
/// <summary>
/// 取对应数字的段落
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
public string GetNumberSentence(int number)
{
var mo = number % _sentences.Count;
// 个位为0,取最后一
if (mo == 0)
{
mo = 10;
}
if (mo == 6)
{
mo = 1;
}
var sentencesIndex = mo - 1;
return _sentences[sentencesIndex];
}
}
上面的方法GetNumberSentence逻辑:传入一个整型number参数,除10(集合_sentences项数)取模,返回10以内的数字美文段落,其中如果模为6会取数字1的段落(这是为了验证拦截逻辑设计添加的)。
下面是写的一个AvaloniaUI程序测试界面,UI不是本文重点,这里就直接贴动图和代码截图了,文末也有源码链接:


2.2. Why is the paragraph with the number 1 always displayed when the single digit is 6?
分析上面的代码,我们想把mo == 6时让mo = 1逻辑去掉,除了使用dnSpy这些反编译工具修改代码,我们还可以使用Lib.Harmony(快学会这个技能-.NET API拦截技法 - Dotnet9)拦截GetNumberSentence方法。
- 安装
Lib.Harmony包
<PackageReference Include="Lib.Harmony" Version="2.3.0-prerelease.2" />
- Write interception replacement classes
参考快学会这个技能-.NET API拦截技法 - Dotnet9添加如下拦截替换类:
- Register the original class type, original method name, and parameter data type that need to be intercepted on the interception class
- 可以先将原方法内代码复制到拦截替换方法
Prefix内,对于原类中的属性、字段可通过反射获取(比如_sentences集合) - 将
mo == 6的代码注释
using HarmonyLib;
using System.Reflection;
using TestDll;
namespace MultiVersionLibrary;
/// <summary>
/// HarmonyPatch特性关联拦截的类及方法
/// </summary>
[HarmonyPatch(typeof(TestTool))]
[HarmonyPatch(nameof(TestTool.GetNumberSentence))]
[HarmonyPatch(new Type[] { typeof(int) })]
internal class HookGetNumberSentence
{
/// <summary>
/// GetNumberSentence拦截替换方法
/// </summary>
/// <param name="__instance">拦截的TestTool实例</param>
/// <param name="number">GetNumberSentence方法同名参数定义,修改它达到方法参数篡改</param>
/// <param name="__result">GetNumberSentence方法返回值,修改它达到方法值伪造</param>
/// <returns></returns>
public static bool Prefix(ref object __instance, int number, ref string __result)
{
try
{
//将原方法逻辑全部复制,然后做部分修改
//1、_sentences是拦截类TestTool的私胡字段,我们通过反射获取值
var sentences =
__instance.GetType().GetField("_sentences", BindingFlags.NonPublic | BindingFlags.Instance)
?.GetValue(__instance) as List<string>;
if (sentences?.Any() != true)
{
__result = "啊,没有优美句子吗?";
return true;
}
var mo = number % sentences.Count;
// 个位为0,取最后一
if (mo == 0)
{
mo = 10;
}
// 2、注释我们认为有歧义的代码
//if (mo == 6)
//{
// mo = 1;
//}
var sentencesIndex = mo - 1;
__result = sentences[sentencesIndex];
return false;
}
catch (Exception ex)
{
return true;
}
}
}
别忘了在Program或App.xaml初始方法内注册自动拦截类方法:
var harmony = new Harmony("https://dotnet9.com");
harmony.PatchAll(Assembly.GetExecutingAssembly());
Rerun the main program again. When entering the number 6, the paragraph corresponding to the number 6 will be displayed normally:

这样就达到不修改第三库源码的情况实现结果篡改了,站长使用.NET 8拦截会有异常,后改为 .NET 6 得以正常运行,异常信息如下,可能是Lib.Harmony还不支持.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 method for obtaining digital paragraphs
还是修改TestTool类,另外增加GetNumberSentence2方法,在方法中添加一个数字验证操作mo = new CalNumber().GetValidNumber(mo);,方法定义如下:
/// <summary>
/// 取对应数字的段落
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
public string GetNumberSentence2(int number)
{
var mo = number % _sentences.Count;
// 个位为0,取最后一
if (mo == 0)
{
mo = 10;
}
// 新增数字验证方法
mo = new CalNumber().GetValidNumber(mo);
var sentencesIndex = mo - 1;
return _sentences[sentencesIndex];
}
The verification method is defined as follows:
CalNumber类和GetValidNumber方法用internal声明,意为类或方法只能在当前工程内使用
internal class CalNumber
{
internal int GetValidNumber(int number)
{
// 这里可以加一些复杂的算法代码
if (number == 6)
{
number = 1;
}
return number;
}
}
And change it to the point where the main project calls the digital acquisition paragraph method:
public string? Number
{
get { return _number; }
set
{
_number = value;
TryParse(_number, out var factNumber);
// 换方法2了
Message = _testTool.GetNumberSentence2(factNumber);
}
}
When entering 6, the paragraph of 1 is returned:

** The question comes: How to intercept the internal method? **
我们不直接注释代码mo = new CalNumber().GetValidNumber(mo);,万一验证方法非常重要,我们只是需要修改其中部分逻辑,总体原逻辑不应该改变。
3.2. How to intercept the internal method?
新增拦截类HookGetValidNumber,现在不能再在类上添加特性了([HarmonyPatch(typeof(CalNumber))]),因为CalNumber不是public访问修饰,跨工程无法直接使用,语法不支持:

If the feature is not used, then manually register the method that needs to be intercepted. This is the focus of this article. The code is below, which is briefly mentioned:
- The manual registration code is similar to the automatic registration declaration features, but it is written differently;
- 拦截替换方法需要使用
HarmonyMethod方法包装; harmony.Patch(hookMethod, replaceHarmonyMethod);用于关联被拦截方法与替换方法
/// <summary>
/// 手工注册关联被拦截方法与替换方法
/// </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:
Prefix方法命名这里不加限制,只要和上面手工注册(var replaceMethod = typeof(HookGetValidNumber).GetMethod(nameof(Prefix));)相同即可:- The number is equal to 6, and the result of modification and counterfeiting is 8
/// <summary>
/// GetNumberSentence拦截替换方法
/// </summary>
/// <param name="__instance">拦截的TestTool实例</param>
/// <param name="number">GetNumberSentence方法同名参数定义,修改它达到方法参数篡改</param>
/// <param name="__result">GetNumberSentence方法返回值,修改它达到方法值伪造</param>
/// <returns></returns>
public static bool Prefix(ref object __instance, int number, ref int __result)
{
//将原方法逻辑全部复制,然后做部分修改
// 这里可以加一些复杂的算法代码
if (number == 6)
{
number = 8;
}
__result = number;
return false;
}
Finally, under the original automatic registration code, add another line of manual registration code to make it OK. After typing, it's a day:
// 1、自动注册拦截类:拦截类上添加被拦截类和方法特性
var harmony = new Harmony("https://dotnet9.com");
harmony.PatchAll(Assembly.GetExecutingAssembly());
// 2、自动注册拦截类,构造被拦截类和方法信息进行拦截
HookGetValidNumber.StartHook();
The operation effect is as follows. Enter 6 to display the number 8 paragraphs:

4. summary
- For technical exchanges and groups, please add webmaster WeChat: codewf
- 文中示例代码:MultiVersionLibrary
使用Lib.Harmony库拦截注册有两种方式的用处如下:
- Automatic registration:
- Automatic registration of interception logic can be achieved by using characteristics on interception classes to associate intercepted classes and method definitions. This method is suitable for situations where there are many classes and methods that need to be intercepted, which can reduce the workload of manual registration and improve development efficiency.
- Automatic registration can usually only associate public classes or methods because the IDE filters and prompts based on the visibility of the code.
- Manual registration:
- The interception logic can be more flexibly controlled by manually registering the intercepted class and method definitions by code construction. This method is suitable for situations where the interception logic needs to be customized. You can select the classes and methods that need to be intercepted according to specific needs, and refine the interception logic.
- Manual registration is more flexible and can intercept various classes and methods, including internal. Manual registration allows you to associate non-public classes and methods by writing code, but it should be noted that doing so may increase code complexity and maintenance costs.