I recently worked with a colleague on implementing support for SearchStax’s Disaster Recovery feature on Sitecore 10.2. While the implementation is not the topic of this post, we faced a peculiar problem when applying it: We needed to implement an internal Sitecore interface. This post will describe how this can be done.
Implementing an internal interface from another assembly is not something you do often. However when implementing support for SearchStax Disaster Recovery on a Sitecore 10.2 solution we opted to implement our own custom initializer based on the Sitecore.ContentSearch.Remote.Http.Abstractions.IHttpRequestInitializer
interface and add it to the list of solrHttpWebRequestFactory
initializers. This interface is internal in Sitecore 10.2 but has been made public in Sitecore 10.3.
While both of us was sure it could be done, we had no clear idea about how. Our first lead was this Stack Overflow thread https://stackoverflow.com/questions/8606412/create-type-that-implements-internal-interface.
This thread suggest using a proxy class, with based on the RealProxy
class and implementing the interface IRemotingTypeInfo
– both from the System.Runtime.Remoting
namespace. As you might know, ‘remoting’ in .NET is the process through which we can access any remote object from one application domain in another application domain. As what we were trying to achieve is not related to remoting, we where initially hesitant to use this approach. However, having tried different approaches with no successs, we returned to the proxy idea, and it turned out to work like a charm.
The proxy approach
The idea suggested on Stack Overflow is actually not to implement the IHttpRequestInitializer
interface, but create a parallel implementation (we will call this the target implementation) and use the remoting capabilities of .NET to create a so-called transparent proxy that mimics the internal interface when presented to the consuming code, while proxying all method calls on to a underlying real proxy, and then further on to our target implementation.
While the example (and the code we ended up creating) does use reflection get the Type
of the internal interface (in our code called the interfaceType
) – this is only used to allow our transparent proxy to present itself to the consuming code as if it was implementing the interface, while actually not doing so. Method calls to the interfaceType
are passed on to the target implementation if a matching method is found (having the same name and parameters). Let us dive into how this can be done.
Implementing a proxy
The first step is to make our proxy implement the IRemotingTypeInfo
inteface, which contains a single method:
public bool CanCastTo(Type fromType, object o) =>
fromType == interfaceType;
As mentioned above we are getting the interfaceType
using reflection – This is straight forward as we already have the Sitecore.ContentSearch
assembly in our application domain:
var interfaceName = "Sitecore.ContentSearch.Remote.Http.
Abstractions.IHttpRequestInitializer";
var assembly = typeof(BaseHttpWebRequestFactory).Assembly;
var interfaceType = assembly.GetType(interfaceName);
So, by implementing the IRemotingTypeInfo
we have a proxy that is potentially able to mimic any interface of our choosing.
Intercepting method calls to our proxy
This of cause require us to handle the calls the consuming code makes when invoking methods on the interface, we a mimicking. To handle these, we make our proxy inherit from the RealProxy
class. This class has a constructor that takes a takes the type that the proxy is supposed to represent which we in our case has gotten access to via reflection.
The RealProxy allow us to implement the Invoke method, and will redirect all invocations to it, allowing us to respond accordantly:
public override IMessage Invoke(IMessage msg)
if (!(msg is IMethodCallMessage methodCall))
throw new NotSupportedException();
if (methodCall.MethodName == "GetType")
{
return new ReturnMessage(this.interfaceType, null, 0,
methodCall.LogicalCallContext, methodCall);
}
return HandleMethodCall(methodCall);
}
In our case, if we are not dealing with method invocations, we are throwing an exception. If we are dealing this a GetType
call, we will return the interfaceType
, adding another layer of our class mimicking the Sitecore interface. All other method calls are passed on to the HandleMethodCall
method:
{
try
{
var result = targetType.GetMethod(methodCall.MethodName).
Invoke(target, methodCall.InArgs);
return new ReturnMessage(result, null, 0, methodCall.
LogicalCallContext, methodCall);
}
catch (TargetInvocationException invocationException)
{
var exception = invocationException.InnerException;
Log.Error($"{nameof(Proxy)} encountered an error",
exception, this);
return new ReturnMessage(exception, methodCall);
}
}
Instantiating the proxy
The last step in making our proxy work is to call the GetTransparentProxy
method on our proxy. This will create a transparent proxy based on our proxy, which is the object that the consuming code will “see.”
This terminology is probably unfamiliar if you have not worked with .NET remoting, but the transparent proxy is a object that will appear to be implementing the interfaceType
, but who’s method calls will be passed on to the underlying RealProxy
implementation (which in our implementation in turn will be pass it on to the target implementation).
Luckily, the initializers in Sitecore uses a factory method. This makes creating the transparent proxy straight forward, and the complete wiring together will look like this:
The patched Sitecore configuration:
<initializers hint="list:AddInitializer">
<connectionleasetimeout type="SDC.Foundation.Indexing.Pipelines.Initializers.SolrHttpRequestInitializerFactory" factorymethod="CreateProxy">
</connectionleasetimeout>
</initializers>
This will result in at call to the factory method CreateProxy
, which in turn will construct a proxy (RealProxy
) and returns the resulting transparent proxy:
public static object CreateProxy()
{
var interfaceName = "Sitecore.ContentSearch.Remote.Http.
Abstractions.IHttpRequestInitializer";
var assembly = typeof(BaseHttpWebRequestFactory).Assembly;
var interfaceType = assembly.GetType(interfaceName);
var target = new TargetImplementation();
return new Proxy(type, target).GetTransparentProxy();
}
And finally, the proxy implementation:
namespace Proxies
{
using Sitecore.Diagnostics;
using System;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting;
public class Proxy : RealProxy, IRemotingTypeInfo
{
private readonly Type interfaceType;
private readonly Type targetType;
readonly object target;
public Proxy(Type type, object target) : base(type)
{
this.target = target;
this.targetType = target.GetType();
this.interfaceType = type;
this.TypeName = type.Name;
}
public override IMessage Invoke(IMessage msg)
{
if (!(msg is IMethodCallMessage methodCall))
throw new NotSupportedException();
if (methodCall.MethodName == "GetType")
{
return new ReturnMessage(this.interfaceType, null,
0, methodCall.LogicalCallContext, methodCall);
}
return HandleMethodCall(methodCall);
}
IMessage HandleMethodCall(IMethodCallMessage methodCall)
{
try
{
var result =
targetType.GetMethod(methodCall.MethodName).
Invoke(target, methodCall.InArgs);
return new ReturnMessage(result, null,
0, methodCall.LogicalCallContext, methodCall);
}
catch (TargetInvocationException invocationException)
{
var exception = invocationException.InnerException;
Log.Error($"{nameof(Proxy)} encountered an error",
exception, this);
return new ReturnMessage(exception, methodCall);
}
}
public bool CanCastTo(Type fromType, object o) =>;
fromType == interfaceType;
public string TypeName { get; set; }
}
}
I hope this will come in handy for others facing similar situations, either in a Sitecore solution or in other .NET projects. And thanks to Morten Sværke Andersen for allowing me to share this work.