7

I would like to get some advice. I am developing a system that will load up plugins at runtime and require them to be available through a WCF endpoint.

I will have a MVC 3 Web app that is only really used for configuration, and a class library (core) that will load up different plugins.

I would appreciate some guidance on how to go about this. I would like to load the plugin up and then be able to create a WCF endpoint that is registered with IIS 7 for access into that plugin.

Thanks in advance :)

Brendon Randall
  • 1,436
  • 3
  • 14
  • 25
  • So you're finding the plugins with MEF correct? Each plugin implements a specific interface? What exactly do you mean by "create a WCF endpoint that is registered with IIS 7 for access into that plugin"? – BrandonZeider Apr 27 '11 at 15:33
  • Well what I have ended up doing is in the actual plugin, define a WCF contact and all the works and start up an endpoint for the plugin. What we ideally wanted to do was try and register that endpoint into IIS 7. But it looks like i will go with the way I am heading at the moment. – Brendon Randall Apr 28 '11 at 14:09
  • You could always self host, that would give you ultimate control over discovered endpoints. – BrandonZeider Apr 28 '11 at 14:23

1 Answers1

11

Using a derivative of Darko's Dynamic IIS hosted WCF Service work, you can achieve something what you want. Let's start with an example service we might want to host, we'll call it an IMessageBroker, it's contract is simple:

[ServiceContract]
public interface IMessageBroker
{
  [OperationContract]
  string Send(string message);
}

We use this contract for both the Service, and the MEF Exports/Imports. We'll also define some additional metadata to go along with it:

public interface IMessageBrokerMetadata
{
  public string Name { get; }
  public string Channel { get; }
}

As it's a simple project, I'll cheat and use a simple static class for managing the MEF CompositionContainer used to compose parts:

public static class MEF
{
    private static CompositionContainer container;
    private static bool initialised;

    public static void Initialise()
    {
        var catalog = new DirectoryCatalog("bin");
        container = new CompositionContainer(catalog);
        initialised = true;
    }

    public static CompositionContainer Container
    {
        get
        {
            if (!initialised) Initialise();
            return container;
        }
    }
}

To be able to generate WCF Services dynamically, we need to create a ServiceHostFactory that can access our composition container to access our types, so you could do:

public class MEFServiceHostFactory : ServiceHostFactory
{
    public override ServiceHostBase CreateServiceHost(string constructorString, System.Uri[] baseAddresses)
    {
        var serviceType = MEF.Container
            .GetExports<IMessageBroker, IMessageBrokerMetadata>()
            .Where(l => l.Metadata.Name == constructorString)
            .Select(l => l.Value.GetType())
            .Single();

        var host = new ServiceHost(serviceType, baseAddresses);

        foreach (var contract in serviceType.GetInterfaces())
        {
            var attr = contract.GetCustomAttributes(typeof(ServiceContractAttribute), true).FirstOrDefault();
            if (attr != null)
                host.AddServiceEndpoint(contract, new BasicHttpBinding(), "");
        }

        var metadata = host.Description.Behaviors
            .OfType<ServiceMetadataBehavior>()
            .FirstOrDefault();

        if (metadata == null)
        {
            metadata = new ServiceMetadataBehavior();
            metadata.HttpGetEnabled = true;
            host.Description.Behaviors.Add(metadata);
        }
        else
        {
            metadata.HttpGetEnabled = true;
        }

        return host;
    }
}

Essentially the constructorString argument is used to pass in the Metadata name we want for the specific service. Next up, we need to handle locating these services. What we now need is a VirtualPathProvider which we can use to dynamically create the instance, through a VirtualFile. The provider would look like:

public class ServiceVirtualPathProvider : VirtualPathProvider
{
    private bool IsServiceCall(string virtualPath)
    {
        virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
        return (virtualPath.ToLower().StartsWith("~/services/"));
    }

    public override VirtualFile GetFile(string virtualPath)
    {
        return IsServiceCall(virtualPath)
                   ? new ServiceFile(virtualPath)
                   : Previous.GetFile(virtualPath);
    }

    public override bool FileExists(string virtualPath)
    {
        if (IsServiceCall(virtualPath))
            return true;

        return Previous.FileExists(virtualPath);
    }

    public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
    {
        return IsServiceCall(virtualPath)
                   ? null
                   : Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
    }
}

What we are doing, is mapping any calls to /Services/ to our MEF derived endpoints. The service needs a virtual file, and this is where we tie it all together:

public class ServiceFile : VirtualFile
{
    public ServiceFile(string virtualPath) : base(virtualPath)
    {

    }

    public string GetName(string virtualPath)
    {
        string filename = virtualPath.Substring(virtualPath.LastIndexOf("/") + 1);
        filename = filename.Substring(0, filename.LastIndexOf("."));

        return filename;
    }

    public override Stream Open()
    {
        var stream = new MemoryStream();
        var writer = new StreamWriter(stream);

        writer.Write("<%@ ServiceHost Language=\"C#\" Debug=\"true\" Service=\"" + GetName(VirtualPath) +
                     "\" Factory=\"Core.MEFServiceHostFactory, Core\" %>");
        writer.Flush();

        stream.Position = 0;
        return stream;
    }
}

The virtual file will break out the Metadata name from the virtual path, where /Services/SampleMessageBroker.svc -> SampleMessageBroker. We then generate some markup which represents the markup of an .svc file with Service="SampleMessageBroker". This argument will be passed to the MEFServiceHostFactory where we can select out endpoints. So, given a sample endpoint:

[Export(typeof(IMessageBroker)),
 ExportMetadata("Name", "SampleMessageBroker"),
 ExportMetadata("Channel", "Greetings")]
public class SampleMessageBroker : IMessagerBroker
{
  public string Send(string message)
  {
    return "Hello! " + message;
  }
}

We can now access that dynamically at /Services/SampleMessageBroker.svc. What you might want to do, is provide a static service which allows you to interegate what endpoints are available, and feed that back to your consuming clients.

Oh, don't forget to wire up your virtual path provider:

HostingEnvironment.RegisterVirtualPathProvider(new ServiceVirtualPathProvider());
Matthew Abbott
  • 60,571
  • 9
  • 104
  • 129