In order to be able to test legacy code which relies on SharePoint, I need to mock some of the objects of SharePoint. I do this by tampering with SharePoint assemblies, replacing their methods by mine on the fly.
This works for some cases, but not for others. A strange situation I encountered is this one.
I want to replace the getter of SPContext.Current
by my own implementation; for the sake of simplicity, my implementation just throws an exception:
.property class Microsoft.SharePoint.SPContext Current()
{
.get class Microsoft.SharePoint.SPContext Proxy.SPContextProxy::get_Current()
}
.method public hidebysig specialname static
class Microsoft.SharePoint.SPContext get_Current () cil managed
{
// Method begins at RVA 0x877e68
// Code size 12 (0xc)
.maxstack 8
IL_0000: nop
IL_0001: ldstr "Proxy don't have an effective implementation of this property."
IL_0006: newobj instance void [mscorlib]System.NotImplementedException::.ctor(string)
IL_000b: throw
} // end of method SPContextProxy::get_Current
When tampering the original assembly, if I replace the IL code corresponding to SPContext.Current
getter, the property cannot be used any longer. I can't even visualize its contents in ILSpy, because this is what is shown instead:
System.NullReferenceException: Object reference not set to an instance of an object.
at Mono.Cecil.Cil.CodeReader.ReadExceptionHandlers(Int32 count, Func`1 read_entry, Func`1 read_length)
at Mono.Cecil.Cil.CodeReader.ReadSection()
at Mono.Cecil.Cil.CodeReader.ReadFatMethod()
at Mono.Cecil.Cil.CodeReader.ReadMethodBody()
at Mono.Cecil.Cil.CodeReader.ReadMethodBody(MethodDefinition method)
at Mono.Cecil.MethodDefinition.<get_Body>b__2(MethodDefinition method, MetadataReader reader)
at Mono.Cecil.ModuleDefinition.Read[TItem,TRet](TRet& variable, TItem item, Func`3 read)
at Mono.Cecil.MethodDefinition.get_Body()
at ICSharpCode.Decompiler.Disassembler.ReflectionDisassembler.DisassembleMethodInternal(MethodDefinition method)
at ICSharpCode.ILSpy.ILLanguage.DecompileProperty(PropertyDefinition property, ITextOutput output, DecompilationOptions options)
at ICSharpCode.ILSpy.TextView.DecompilerTextView.DecompileNodes(DecompilationContext context, ITextOutput textOutput)
at ICSharpCode.ILSpy.TextView.DecompilerTextView.<>c__DisplayClass16.<DecompileAsync>b__15()
On the other hand, when I insert my instructions before the original instructions, I can call the getter successfully, as well as see its contents in ILSpy:
.property class Microsoft.SharePoint.SPContext Current()
{
.custom instance void [Microsoft.SharePoint.Client.ServerRuntime]Microsoft.SharePoint.Client.ClientCallableAttribute::.ctor() = (
01 00 00 00
)
.get class Microsoft.SharePoint.SPContext Microsoft.SharePoint.SPContext::get_Current()
}
.method public hidebysig specialname static
class Microsoft.SharePoint.SPContext get_Current () cil managed
{
// Method begins at RVA 0x33e2d8
// Code size 61 (0x3d)
.maxstack 1
.locals init (
[0] class Microsoft.SharePoint.SPContext,
[1] class [System.Web]System.Web.HttpContext,
[2] class Microsoft.SharePoint.SPContext
)
... follows by the instructions that I inserted:
IL_0000: nop
IL_0001: ldstr "Proxy doesn't implement this property yet."
IL_0006: newobj instance void [mscorlib]System.NotImplementedException::.ctor(string)
IL_000b: throw
... follows by the original instructions:
IL_000c: ldnull
IL_000d: stloc.0
IL_000e: call class [System.Web]System.Web.HttpContext [System.Web]System.Web.HttpContext::get_Current()
IL_0013: stloc.1
IL_0014: ldloc.1
IL_0015: brfalse.s IL_0039
.try
{
IL_0017: ldloc.1
IL_0018: call class Microsoft.SharePoint.SPWeb Microsoft.SharePoint.WebControls.SPControl::GetContextWeb(class [System.Web]System.Web.HttpContext)
IL_001d: brtrue.s IL_0023
IL_001f: ldnull
IL_0020: stloc.2
IL_0021: leave.s IL_003b
IL_0023: leave.s IL_002a
} // end .try
catch [mscorlib]System.InvalidOperationException
{
IL_0025: pop
IL_0026: ldnull
IL_0027: stloc.2
IL_0028: leave.s IL_003b
} // end handler
.try
{
IL_002a: ldloc.1
IL_002b: call class Microsoft.SharePoint.SPContext Microsoft.SharePoint.SPContext::GetContext(class [System.Web]System.Web.HttpContext)
IL_0030: stloc.0
IL_0031: leave.s IL_0039
} // end .try
catch [mscorlib]System.IO.FileNotFoundException
{
IL_0033: pop
IL_0034: leave.s IL_0039
} // end handler
catch [mscorlib]System.InvalidOperationException
{
IL_0036: pop
IL_0037: leave.s IL_0039
} // end handler
IL_0039: ldloc.0
IL_003a: ret
IL_003b: ldloc.2
IL_003c: ret
} // end of method SPContext::get_Current
What prevents the code from being loaded by ILSpy when original instructions are removed before new ones are inserted?
Notes:
Tampering is done with Mono.Cecil by using MethodDefinition.Body.Instructions
collection (and the corresponding Insert
and Remove
methods.)
A few other methods and properties of Microsoft.SharePoint
assembly are tampered successfully: ILSpy displays the resulting IL code.
I thought that .maxstack
directive could be a problem (1 in the original property, 8 in the proxied one, 1 in the result). After a few tests on a separate project, it appears that it has no effect.
I also suspected that exceptions could be the cause (the original code throws different exceptions than the new one). After a few tests on a separate project, it appears that it has no effect either.
When IL is shown in textual form, exception handling blocks (.try
, catch
, etc.) appear as actual blocks of IL instructions, just like they do in C#.
But in the binary form, exception handling blocks are stored separately (see §II.25.4.6 Exception handling clauses of ECMA-335) and reference the IL instructions using offsets. In Cecil, exception handlers are represented using the MethodBody.ExceptionHandlers
property.
So, if you replaced the old MethodBody.Instructions
with your own instructions, it's very likely that the offsets of the old exception handlers are now invalid, which is causing the issues. (The fact that Cecil throws NullReferenceException
sounds like a bug to me, consider reporting it.)
The other example that you linked to which doesn't exhibit this problem is different because there the original method doesn't contain exception handlers, it throws an exception. And throw
is just a normal IL instruction, it doesn't have a special representation like e.g. .try
/catch
does.