Thursday, February 2, 2012

Autofac and A Simple Plugin Design

I am working on a web service that needs to support custom processing components depending on the input. I had started using Autofac as the DI container for the initial service implementation, but it wasn't until I required the custom processing components that I really needed DI.

The plan was to generate the components as independent assemblies so that they could be implemented and deployed independantly of the service. This also meant I needed XML configuration to register each new component.

The implementation would be to instantiate a Service, which had an IProcessor as a dependency

public Service(Common commonService, IProcessor processor)

I then register the service as a named service, which is named after the processor that is to be created. However, the named server needed to be configured to create a named instance of the IProcessor when resolved (as the WebService resolves the Service, not the IProcessor).

builder.Register(c => new Service(c.Resolve<TrimCommon>(), c.ResolveNamed<IProcessor>("CustomProcessor"))).Named<Service>("CustomProcessor");

In order to resolve the Service I need to register my CustomProcessor, which is where the 'trickyness' kicks in. I didn't want to add each CustomProcessor as a reference to my service, as that would be difficult to manage, so I could not just register the type Fluently.
This meant I had to go back to XML (eewww, spring.net) to register my IProcessor implementations. This was still very easy
<component type="CustomProcessor, CustomProcessor" service="IProcessor, Common" name="CustomProcessor" instance-scope="per-lifetime-scope" />

So in code, when I resolve
Service service = getInstanceContext().ResolveNamed("CustomProcessor");
I get a Service with the appropriate processor.

Unfortunately I realised that when trying to resolve the CustomProcessor class this failed, as it was not loaded into the AppContext, and did not exist in the GAC. To fix this I had to handle the AppDomain.CurrentDomain.AssemblyResolve event and load the assembly manually.

The code below handles the event and loads the assembly if it exists in the "plugins" folder of the running webservice

Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
System.IO.DirectoryInfo folder = new System.IO.DirectoryInfo(this.Server.MapPath("/plugins"));
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
System.IO.FileInfo[] files = folder.GetFiles("*.dll");
foreach (System.IO.FileInfo file in files)
{
try
{
Assembly assembly = Assembly.LoadFrom(file.FullName);
if (assembly.FullName.Substring(0, assembly.FullName.IndexOf(',')) == args.Name)
{
return assembly;
}
}
catch (Exception)
{
return null;
}
}

return null;
}

No comments:

Post a Comment