Creating SignalR Hub Classes on the fly

Recently I wanted to create SignalR hubs for client notification based on a list of internal queues. As SignalR is creating Hubs based on their type names I had to find an approach for dynamically defining classes based on the given queue list. In this blog post I show a way on how to do this

The Situation

In our project we have a list of queues that hold various information to be processed by various components. However this list is not static or fixed, which means that an arbitrary component in our system could potentially define a new queue.

public class Hub
{
  public static IDictionary<string, CircularQueue<WorkItem>> Queues;

  static Hub()
  {
    Queues = new Dictionary<string, CircularQueue<WorkItem>>
    {
      {"Net.Appclusive.Core.Messaging.Hub.Default", new CircularQueue<WorkItem>()},
      {"Net.Appclusive.Core.Messaging.Hub.Location.Emea", new CircularQueue<WorkItem>()},
      {"Net.Appclusive.Core.Messaging.Hub.Location.Amer", new CircularQueue<WorkItem>()},
      {"Net.Appclusive.Core.Messaging.Hub.Location.Asia", new CircularQueue<WorkItem>()}
    };
  }
}

The Problem

Now I wanted for each WorkItem in any of our queues a notification to be sent to a specific SignalR hub. A typical SignalR hub in our environment would look similar to this:

public class Emea : Hub<IWorkerClient>, IWorkerHub
{
  public async void NotifyServer(WorkItem workItem)
  {
    // do some processing
  }
}

GlobalHost.ConnectionManager.GetHubContext<Emea, IWorkerClient>();

The name of such a hub class would then be equal to the hub class name, Emea in this example.

The Solution

However, as the list of queues was dynamic I could not create all the classes beforehand. So the plan was to define a hub base class from which I could derive a new class that should be created at runtime prior to the registration of SignalR. Enter Reflection.Emit.

With the classes in Reflection.Emit it is possible to define new types and classes at runtime and save them into a dynamic module. So I wanted to create something like this – at runtime:

public class WorkerHubBase : Hub<IWorkerClient>, IWorkerHub
{
  public async void NotifyServer(WorkItem workItem)
  {
    // do some processing
  }
}

public class Emea : WorkerHubBase
{
  // N/A
}

GlobalHost.ConnectionManager.GetHubContext<Emea, IWorkerClient>();

Creating such an assembly was easier than I first thought. All I had to do, was creating a module inside an assembly and add a new class type via DefineType and CreateType.

typeMap = new Dictionary<string, Type>();

var path = Path.GetTempPath();
var assemblyName = string.Concat(GetType().Namespace, NAMESPACE_SEPARATOR, Path.GetRandomFileName());
var assemblyFileName = assemblyName + FILE_EXTENSION;
var asmName = new AssemblyName(assemblyName);
var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave, path);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName, assemblyFileName);

foreach (var hubName in hubNames)
{
  var typeBuilder = moduleBuilder.DefineType(hubName, TypeAttributes.Public, typeof(THubBase));
  var type = typeBuilder.CreateType();
  typeMap.Add(hubName, type);
}

assemblyBuilder.Save(assemblyFileName);

One thing to note here is, that (especially when running on a web server) we have to use a path that is writable, otherwise we get an ACCESS_DENIED error when invoking assemblyBuilder.Save().

After this assembly has been created (with our hub classes. We have to get their contexts, so we can notify the client about new work items.
This can also easily be achieved by using little bit of reflection:

var result = new Dictionary<string, IHubContext<THubClient>>();

var methodInfos = GlobalHost.ConnectionManager.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance);
var genericMethodInfoGetHubContext = methodInfos.FirstOrDefault(e => 
  e.IsGenericMethod 
  && e.ContainsGenericParameters 
  && 2 == e.GetGenericArguments().Length
  && typeof(IHub).IsAssignableFrom(e.GetGenericArguments().First())
  && e.Name == nameof(ConnectionManager.GetHubContext));
Contract.Assert(null != genericMethodInfoGetHubContext);

foreach (var kvp in typeMap)
{
  var hubName = kvp.Key;
  var hubType = kvp.Value;

  var methodInfo = genericMethodInfoGetHubContext.MakeGenericMethod(hubType, typeof(THubClient));
  var hubContext = (IHubContext<THubClient>) methodInfo.Invoke(GlobalHost.ConnectionManager, null);

  result.Add(hubName, hubContext);
}

Note: this step has to occur AFTER we called app.MapSignalR(hubConfiguration);. However this will only work when we supply our own DependencyResolver which we register upon instantiation of the HubConfiguration:

var hubConfiguration = new HubConfiguration
{
  Resolver = hubFactory.DependencyResolver,
};
app.MapSignalR(hubConfiguration);

The resolver actually has to override GetServices and replace the instance for IHubDescriptorProvider. In that provider we will then do a lookup for the correct hub based on the hub name:

public override IEnumerable<object> GetServices(Type serviceType)
{
  var services = base.GetServices(serviceType).ToList();
  if (typeof(IHubDescriptorProvider).IsAssignableFrom(serviceType))
  {
    services.Add(new HubFactoryHubDescriptorProvider(typeMap));
  }
  return services;
}

Inside our IHubDescriptorProvider implementation I only implement TryGetHub:

public bool TryGetHub(string hubName, out HubDescriptor descriptor)
{
  if (!typeMap.ContainsKey(hubName))
  {
    descriptor = default(HubDescriptor);
    return false;
  }

  descriptor = new HubDescriptor
  {
    Name = hubName,
    NameSpecified = true,
    HubType = typeMap[hubName]
  };
  return true;
}

Now we are ready and can create special hub classes based on the internal queues.

The complete code can be found here on GitHub.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: