Today’s post title is rather lengthly so I keep the introduction short and come right to the point: when using OData (or any web services therefore) it can become quite hard to keep the actual business logic separate and not to mix things where they do not belong. At least for me, as I have to admit that this happened to us in one of our projects. And when we wanted to upgrade from ODATA v3 to ODATA v4 we realised that we have to re-factor quite a few things. So there had to be a better approach.
With StructureMap it is very easy to use IoC with WebApi via the WebApi.StructureMap package. So all we have to do is to define a constructor that accepts our business logic and inject it.
For this to work we need a common interface that we can define on our controllers. In our case we dsigned our system to have a set of entities which all derive from a common base entity:
public class BaseEntity { long Id { get; set; } string Name { get; set; } } public class Order : BaseEntity { DateTimeOffset DeliveryDate { get; set; } } public class Item : BaseEntity { Guid TrackingId { get; set; } }
Our business logic would be handled on an entity basis via a Manager and all of our managers should support the following basic CRUD operations:
- C New
- R Get
- R GetAll
- U Update
- D Remove
For the naming we decided to have the following convention in place:
- every Manager would be called #Entity#Manager, e.g.
OrderManager
,ItemManager
- every Controller would be called #Entity#sController, e.g.
OrdersController
,ItemsController
(following ODATA naming conventions)
We then defined an interface for our managers:
public interface IEntityManager<T> where T : BaseEntity { IEnumerable<T> Get(); T Get(long id); T New(T entityToBeCreated); T Update(T modifiedEntity, T originalEntity); T Update(T entityToBeUpdated, DictionaryParameters delta); void Remove(T entityToBeDeleted); }
With this we could define our controllers like this:
public class OrdersController : OdataController { private readonly IEntityManager<Order> entityManager; public OrdersController(IEntityManager<Order> entityManager) { Contract.Requires(null != entityManager); this.entityManager = entityManager; } // ... } public class ItemsController : OdataController { private readonly IEntityManager<Item> entityManager; public ItemsController(IEntityManager<Item> entityManager) { Contract.Requires(null != entityManager); this.entityManager = entityManager; } // ... }
We then needed a special registration convention for our controllers:
public class ControllerRegistrationConvention : IRegistrationConvention { public void ScanTypes(TypeSet types, Registry registry) { Contract.Requires(null != types); Contract.Requires(null != registry); types .FindTypes(TypeClassification.Concretes | TypeClassification.Closed) .Where(e => e.FullName.EndsWith("Controller") && typeof(OdataController).IsAssignableFrom(e)) .ForEach(controllerType => { Register(controllerType, registry); } } public void Register(Type controllerType, Registry registry) { // ... } }
With this convention we could tell WebApi to use this for our ODATA controllers:
public static class IoC { public static Action<ConfigurationExpression> DefaultConfiguration { get { return map => { map.Scan(scanner => { scanner.TheCallingAssembly(); scanner.WithDefaultConventions(); scanner.Convention<ControllerRegistrationConvention>(); }); }; } } } public class WebApiApplication : System.Web.HttpApplication { protected void Application_Start() { GlobalConfiguration.Configuration.UseStructureMap(IoC.IoC.DefaultConfiguration); } }
Now the only things that was left was to do the mapping from the generic entity manager to the concrete manager type with a little bit of reflection:
private void Register(Type controllerType, Registry registry) { controllerType.GetConstructors().Any(ctor => { // 1. we are only interested in constructors with a single parameter var parameters = ctor.GetParameters(); if (1 != parameters.Length) { return false; } // 2. and the parameter must be a generic var parameterType = parameters.First().ParameterType; if (!parameterType.IsGenericType) { return false; } // 3. that generic must have only a single argument var genericArguments = parameterType.GetGenericArguments(); if (1 != genericArguments.Length) { return false; } var genericArgument = genericArguments.First(); // 4. the base type of that generic type must derive from BaseEntity if (genericArgument.BaseType != typeof(BaseEntity)) { return false; } // 5. now resolve the actual manager // we *know* the manager resides in the same assembly as IEntityManager<> var managerType = typeof(IEntityManager<>).Assembly.DefinedTypes .FirstOrDefault(e => e.IsGenericType && 1 == e.GenericTypeParameters.Length && e.GenericTypeParameters.First() .GetGenericParameterConstraints().First().Name == genericArgument.Name && e.Name.StartsWith(string.Concat(genericArgument.Name, "Manager")) ); Contract.Assert(null != managerType, controllerType.FullName); // 6. now we create the generic type to be injected var genericManagerType = managerType.MakeGenericType(genericArgument); // 7. and register the mapping registry.For(parameterType).Use(genericManagerType); // it's as easy as that return true; }); }
We can verify this with a simple unit test:
[TestMethod] public void DefaultConfigurationIsValid() { var sut = new Container(WebApi.IoC.IoC.DefaultConfiguration); Trace.WriteLine(sut.WhatDoIHave()); sut.AssertConfigurationIsValid(); }
The output of WhatDoIHave will then look similar to this:
================================================================================== PluginType Namespace Description ---------------------------------------------------------------------------------- Func<TResult> System Open Generic Template for Func<> ---------------------------------------------------------------------------------- Func<T, TResult> System Open Generic Template for Func<,> ---------------------------------------------------------------------------------- IContainer StructureMap Object: StructureMap.Container ---------------------------------------------------------------------------------- IEntityManager<Order> Net.Appclusive.Core.Domain OrderManager<Order> ---------------------------------------------------------------------------------- IEntityManager<Item> Net.Appclusive.Core.Domain ItemManager<Item> ---------------------------------------------------------------------------------- Lazy<T> System Open Generic Template for Func<> ==================================================================================
With this in place we completely decoupled the business logic from the transport protocol and could upgrade to ODATA v4 (or any other means).