Tampering SharePoint assemblies part 1
I've inherited a SharePoint project and I'm at the step when I start adding tests for regression testing. If the authors of the code knew anything about object orientation and design, there would be no problems at this step; but, as usual, they were slightly ignorant of the very basics of programming, and so nearly every method relies on SharePoint classes such as SPSite
or SPWeb
. If those classes were based on interfaces, creating the corresponding mocks would be relatively easy, but unfortunately, Microsoft didn't thought that someone could ever need to test the code of SharePoint applications.
So came the search for a solution. I already knew that Microsoft did a great job with Microsoft Fakes, and, in the context of SharePoint, with SharePoint.Emulators which are based on Microsoft Fakes and contain the mocks of SP
classes. Unfortunately, it doesn't work with .NET Framework 4 or later, and in all cases needs Visual Studio Ultimate, which would probably be problematic in a context of the current project:
System Requirements [...] Microsoft Visual Studio 2012 Ultimate
Hopefully, there is a third-party alternative for SharePoint 2013 and .NET Framework 4.5: SPEmulators. Nice. But it doesn't work well. If all the tested code needs is to access simple stuff, like the name of the site, everything works flawlessly. But as soon as you start doing chains such as site.WebApplication.AlternateUrls
, you end up with null
objects that you can't replace by something else.
Since the solutions designed specifically to mock SharePoint classes didn't work, it was time to start using the heavy artillery: proxies and AOP frameworks.
.NET Framework proxies
When I used .NET Framework proxies in the past, I had the control over the proxied objects, and so inheriting the classes of those objects from MarshalByRefObject
and eventually making them serializable wasn't a problem. This solution couldn't be applied in the current case, since I wouldn't be able to change the inheritance of the classes of SharePoint assemblies in the first place.
Castle Windsor DynamicProxy
So I started by the third-party proxy library I [thought I] knew: Castle Windsor's DynamicProxy. I started by setting up a simple project to test the feasibility of the thing. The project was a simple test project with a single file containing everything I needed, and a single test: green means that I found the solution; red means I failed.
namespace UnitTests
{
using Castle.DynamicProxy;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
[TestClass]
public class DemoTests
{
[TestMethod]
public void TestProxy()
{
var generator = new ProxyGenerator();
var proxy = generator.CreateClassProxy<Demo>(new DemoInterceptor());
var actual = proxy.SayHello();
var expected = "Something else";
Assert.AreEqual(expected, actual);
}
}
[Serializable]
public class DemoInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
invocation.ReturnValue = "Something else";
}
}
public class Demo
{
public string SayHello()
{
return "Hello, World!";
}
}
}
The test failed. Thanks to Sriram Sakthivel, I learned that it won't work anyway: I would need either an interface or a virtual method to make this work.
SpringAOP
SpringAOP has the very same problem. The approach is similar; the interceptor looks like this:
public class DemoInterceptor : IMethodInterceptor
{
public object Invoke(IMethodInvocation invocation)
{
return "Something else";
}
}
and the test looks like that:
var factory = new ProxyFactory(new Demo());
factory.AddAdvice(new DemoInterceptor());
var proxy = (Demo)factory.GetProxy();
var actual = proxy.SayHello();
var expected = "Something else";
Assert.AreEqual(expected, actual);
but the functioning remains the same, and the result is the same as well: the test fails, but succeeds if SayHello
is virtual.
AspectSharp
AspectSharp seems to be just a layer of abstraction based on Castle's DynamicProxy. I'm pretty sure it has the same limitations as DynamicProxy.
PostSharp
PostSharp seemed more promising, since not being limited to virtual methods, but once again, required to modify the proxied code itself by decorating either the classes or the specific methods with an attribute which, by inheriting from OnMethodBoundaryAspect
, signals to PostSharp that the class or method is a part of the AOP strategy.
Mono.Cecil
Since AOP frameworks are too limited, it was time to push a little further: instead of extending classes through AOP, what if we could tamper them?
Reflection.Emit
being quite difficult to grasp, I found it more convenient to use Mono.Cecil. Well, maybe convenience is the least appropriate term when it comes to Reflection and especially dynamic alteration of assemblies, but at least, it's damn effective.
For those who may ask themselves if it's even possible (could you imagine the implications of that? Someone could go and change the effective implementation of string.IsNullOrEmpty
to return false
every time just for fun, or, slightly more subtle, change SecureString
to also send sensitive information to a remote server. Moreover, what about signed assemblies?
In fact, it is perfectly possible to change an assembly. You can even tamper with an assembly in GAC using a text editor. This is because they are checked during installation, not every time they are called.
The actual solution, however, is not particularly straightforward. We can't just put everything in the same assembly and expect it to work magically. Let me explain the overall architecture of the solution.
I still have my test project, but it cannot reference directly the assembly which should be tampered (let's call it Target
). If it does, this means that whenever I start the project, it will read the original, referenced assembly, and lock the file, preventing me from modifying it. Instead, I pass by an intermediary project which, in turn, references the one which will be tampered. This intermediary, let's call it Consumer
, simply calls a method from the assembly which is subject to tampering; in the same way, the assemblies under test of an actual project call SharePoint assemblies, without the need for unit tests to reference those SharePoint assemblies.
When the test starts, it begins by tampering the target assembly, and only then loads a consumer within an AppDomain
and launches the action which uses the tampered target.
The unit test within the DemoTests
class looks just like the one in the previous AOP example:
[TestMethod]
public void TestProxy()
{
var originalPath = @"H:\Samples\TamperingWithCecil\Target\bin\Debug\Target.dll";
var replacement = typeof(DemoTests).GetMethod("SayHelloReplacement");
var proxy = CreateProxy(originalPath, "Demo", "SayHello", replacement);
var consumer = this.CreateConsumer<Consumer>();
var actual = consumer.Talk(proxy);
var expected = "Something else";
Assert.AreEqual(expected, actual);
}
public string SayHelloReplacement()
{
return "Something else";
}
This is a prototype, which means that many things are far from being as I would like them to be. In particular, many elements are noticeably stringly-typed.
The method which creates the consumer (currently in the same DemoTests
class) is simply loading the assembly in the current AppDomain
and creating an instance of the class:
private dynamic CreateConsumer<T>()
{
var consumerAssemblyPath = this.GetAssemblyFilePath(typeof(T));
AppDomain.CurrentDomain.Load(File.ReadAllBytes(consumerAssemblyPath));
return AppDomain.CurrentDomain.CreateInstanceAndUnwrap(
typeof(T).Assembly.FullName, typeof(T).FullName);
}
The method which does the most interesting part—the tampering of the assembly—is more complicated, and I sincerely apologize for such ugliness. Both this method, the previous one and the few ones which will follow belong to a separate library and are in DemoTests
just for the convenience of this early prototype.
private dynamic CreateProxy(
string proxiedAssemblyPath, string proxiedTypeName,
string proxiedMethodName, MethodInfo replacement)
{
var originalPath = proxiedAssemblyPath;
var alternatePath = Path.Combine(
Path.GetDirectoryName(GetAssemblyFilePath()),
Path.GetFileName(originalPath));
File.Copy(originalPath, alternatePath, overwrite: true);
var module = ModuleDefinition.ReadModule(alternatePath);
var method = this.FindMethod(module, proxiedTypeName, proxiedMethodName);
var replacementMethod = this.FindMethod(
this.GetAssemblyFilePath(replacement.DeclaringType),
replacement.DeclaringType.Name, replacement.Name);
var processor = method.Body.GetILProcessor();
var firstTargetInstruction = method.Body.Instructions.First();
foreach (var newInstruction in replacementMethod.Body.Instructions)
{
processor.InsertBefore(firstTargetInstruction, newInstruction);
}
module.Write(alternatePath);
var assembly = Assembly.LoadFile(alternatePath);
var type = assembly.GetType(
Path.GetFileNameWithoutExtension(proxiedAssemblyPath) +
"." + proxiedTypeName);
return Activator.CreateInstance(type);
}
Finally come a bunch of small methods used by two previous methods:
private string GetAssemblyFilePath()
{
return this.GetAssemblyFilePath(Assembly.GetExecutingAssembly());
}
private string GetAssemblyFilePath(Type type)
{
return this.GetAssemblyFilePath(type.Assembly);
}
private string GetAssemblyFilePath(Assembly assembly)
{
var uri = new UriBuilder(assembly.CodeBase);
return Uri.UnescapeDataString(uri.Path);
}
public MethodDefinition FindMethod(
string modulePath, string typeName, string methodName)
{
var module = ModuleDefinition.ReadModule(modulePath);
return this.FindMethod(module, typeName, methodName);
}
public MethodDefinition FindMethod(
ModuleDefinition module, string typeName, string methodName)
{
var type = module.Types.Single(c => c.Name == typeName);
return type.Methods.Single(c => c.Name == methodName);
}
The consumer (which is in the separate consumer assembly) contains the following class:
public class Consumer
{
public string Talk(Demo demo)
{
return demo.SayHello();
}
}
This approach not only works for any method, would it be virtual or not, but is also perfectly viable for testing, especially testing code which overly-relies on third-party assemblies, such as SharePoint.
There is still a lot of work until this approach could be used to create mocks. It is also very slow (one second to run a single test), and it would be probably much, much slower when used with an assembly of the size of Microsoft.SharePoint
, but despite this, this prototype shows a possible solution to the problem of unit testing of legacy code based on SharePoint or any other assemblies which are not designed to be easily replaceable.