Anatomy of an API plugin
The Enterprise Tester application is extensible - so not only the core plugin, but other plugins, need to contribute to the overall API exposed.
This wasn't particularly complex to implement - but we did have a few issues we had to circumvent.
Controller Registration
We took a pretty restrictive approach to registration of API controllers - plugin developers are able to register the controller along with a name and a route (optionally you could also specify defaults and constraints, but this generally wasn't necessary) - but we don't allow one route to service multiple controllers.
public class CoreRestResourcesPluginInstaller : AbstractPluginInstaller
{
const string ApiPath = "api/";public override void Install(InstallationHelper helper)
{
helper.RegisterRestService("project", ApiPath + "project/{id}");
helper.RegisterRestService("projects", ApiPath + "projects");
...
}
}
This approach makes the process of writing the API for your plugin alot easier in many ways as you can be generally assured your plugin's routes wont clash with other plugins (and where a clash exists we can throw up a meaningful error/warning, describing where the conflict exists) - but it won't win any fans with the convention over configuration purest crew.
Notice also that registering a REST service does not restrict the route to start with /api - we did originally enforce that convention, but then relaxed it because not all WCF WebAPI controllers being registered would necessarily be part of the API for a plugin.
As a result of these decisions, you end up often with 2 controllers - one for the resource representing an individual entity, and another for the collection resource - there was an interesting discussion about this in February of this year between Rob Conery, some of the more Zealous members of the REST community and eventually Glenn Block - which is well worth a read.
In a future post I'm going to cover how I demo REST API's to customers (and strongly encourage feedback on if anybody knows better ways to do this - short of writing sample clients for your API) - as this can be another one of those exercises where going through the process of demonstrating the API interactively (even with non-developers) hilights issues with your API design you won't necessarily discover through testing.
Delayed Registration
One of the other challenges we faced is that in our implementation we expose the methods for registering REST services as part of our plugin framework (so it's part of the core) - but our REST framework (which exists in it's own plugin) takes dependencies on all sorts of thing such as our OAuth, Search, Custom Field etc. plugins - and some of those plugins may even want to register their own API controllers (causing circular reference issues).
To get around this the RegisterRestService extension method adds the registration to a static "ServiceManager" instance, which then takes care of either registering the route etc. immediately if the REST infrastructure is in-place, or collects the registration information awaiting the REST infrastructure being ready as part of application start-up process.
public static class RestServiceRegistrationExtensions
{
public static InstallationHelper RegisterRestService(this InstallationHelper helper, string name, string routeTemplate, object defaults = null, object constraints = null)
where T : class
{
helper.Register(Component.For().LifeStyle.Transient); ServiceManager.Instance.RegisterService
(name, routeTemplate, defaults, constraints); return helper;
}
}
Here's the ServiceManager:
public class ServiceManager : IServiceManager
{
static IServiceManager _serviceManager = new ServiceManager();
readonly IList_services = new List ();
Action_callback; public static IServiceManager Instance
{
get { return _serviceManager; }
}public void RegisterService
(string name, string routeTemplate, object defaults = null, object constraints = null)
{
var metadata = new ServiceMetadata {RouteTemplate = routeTemplate, ControllerType = typeof (T), Defaults = defaults, Constraints = constraints, Name = name};_services.Add(metadata);
if (_callback != null)
{
_callback(metadata);
}
}public IList
GetAllServices()
{
return _services;
}public void SetServiceRegisteredCallback(Action
callback)
{
if (callback == null) throw new ArgumentNullException("callback");if (_callback == null)
{
foreach (ServiceMetadata service in _services) callback(service);
}_callback = callback;
}public static void Reset()
{
_serviceManager = new ServiceManager();
}
}
Nothing particular exciting here but it's something to consider if trying to support WebAPI in a pluggable application. Also notice the Reset() method which is necessary to support the end-to-end tests.
Mapping Installers
As well as controllers, we have classes which register any necessary functionality against the ViewModelMapper - we normally have one of these per resource type.
Instances of the mapping installer are automatically registered into the IoC container, and implement an IStartable lifestyle in Windsor (so will be immediately created once all the dependencies are satisfied).
public class SomeMappingInstaller : AbstractViewModelMappingInstaller
{
public override void Install(IViewModelMapper mapper)
{
...
}
}
This is necessary (Rather then registering the mappings directly in the plugin installation method) because often the mapping logic will need to get access to other services to be able to complete an expansion from one Entity type or another (in which case you would add a constructor where the necessary services could be injected upon creation of the mapping installer).
And that's it...
As you can see we have tried to keep the number of moving parts to a minimum when adding an API controller to a plugin for Enterprise Tester as a 3rd party developer.
The other desirable trait that comes from this minimalism is that 3rd party developers can't introduce side-effects into other API controllers accidentally (i.e. by adding a new DelegatingHandler, or removing an existing one - such as Authentication).
Next
Next in part 9 of this series (The final part) - I give my thoughts on how developing the API using ASP.Net Web API went, including the experience of transition from preview/RC/Beta bits and the recent upgrade to RTM, plus what we didn't get to implement/will be implementing in the future for the API in Enterprise Tester.