Tuesday, December 30, 2008

ServiceLocator redivivus

This is the (so far) final form of the ServiceLocator class:


  1. using System;  
  2. using System.Collections.Generic;  
  3. using System.Linq;  
  4. using System.Reflection;  
  5.   
  6. namespace Helper  
  7. {  
  8.   public static class ServiceLocator  
  9.   {  
  10.     private static readonly Dictionary<Type, Func<object>> container = new Dictionary<Type, Func<object>>();  
  11.     private static readonly object containerLock = new object();  
  12.   
  13.     public static Func<Type, object> OnTypeNotFound =  
  14.       type => { throw new NotSupportedException("Cannot resolve type " + type); };  
  15.   
  16.     /// <summary>  
  17.     /// Discovers all the classes decorated with the Register attribute and registers them as implementations of the ImplementedType argument  
  18.     /// </summary>  
  19.     public static void Discover()  
  20.     {  
  21.       var types = Assembly.GetCallingAssembly().GetTypes().ToList();  
  22.       types.ForEach(type =>  
  23.                       {  
  24.                         var attr = type.GetCustomAttribute<RegisterAttribute>();  
  25.                         if (attr != null)  
  26.                           Register(attr.ImplementedType, () => Activator.CreateInstance(type));  
  27.                       });  
  28.     }  
  29.   
  30.     /// <summary>  
  31.     /// Helper function to return a custom attribute of a specified type  
  32.     /// </summary>  
  33.     /// <typeparam name="T">Desired type for the custom attribute</typeparam>  
  34.     /// <param name="type">Type searched for the custom attribute</param>  
  35.     /// <returns>The custom attribute or null if it was not found</returns>  
  36.     private static T GetCustomAttribute<T>(this ICustomAttributeProvider type) where T : class  
  37.     {  
  38.       return type.GetCustomAttributes(typeof (T), true).FirstOrDefault() as T;  
  39.     }  
  40.   
  41.     /// <summary>  
  42.     /// Maps the type T to a function returning T  
  43.     /// </summary>  
  44.     /// <typeparam name="T">Type requested by the application</typeparam>  
  45.     /// <param name="func">Function returning an instance of T</param>  
  46.     public static void Register<T>(Func<T> func) where T : class  
  47.     {  
  48.       Register(typeof (T), () => func());  
  49.     }  
  50.   
  51.     /// <summary>  
  52.     /// Maps the type T to a function returning T  
  53.     /// </summary>  
  54.     /// <param name="T">Type requested by the application</param>  
  55.     /// <param name="func">Function returning an instance of T</param>  
  56.     public static void Register(Type T, Func<object> func)  
  57.     {  
  58.       if (Contains(T))  
  59.         throw new ArgumentException("Type " + T + " already registered.");  
  60.   
  61.       lock (containerLock)  
  62.         container.Add(T, func);  
  63.     }  
  64.   
  65.     /// <summary>  
  66.     /// Removes the association for type T (for example as a preparation for another registration)  
  67.     /// </summary>  
  68.     /// <typeparam name="T">Type to unregister</typeparam>  
  69.     public static void Unregister<T>() where T : class  
  70.     {  
  71.       Unregister(typeof (T));  
  72.     }  
  73.   
  74.     /// <summary>  
  75.     /// Removes the association for type T (for example as a preparation for another registration)  
  76.     /// </summary>  
  77.     /// <param name="T">Type to unregister</param>  
  78.     public static void Unregister(Type T)  
  79.     {  
  80.       lock (containerLock)  
  81.         container.Remove(T);  
  82.     }  
  83.   
  84.     /// <summary>  
  85.     /// Returns true if the type T is registered  
  86.     /// </summary>  
  87.     /// <typeparam name="T">Type to check for an existing registration</typeparam>  
  88.     /// <returns>True if T is registered</returns>  
  89.     public static bool Contains<T>() where T : class  
  90.     {  
  91.       return Contains(typeof (T));  
  92.     }  
  93.   
  94.     /// <summary>  
  95.     /// Returns true if the type T is registered  
  96.     /// </summary>  
  97.     /// <param name="T">Type to check for an existing registration</param>  
  98.     /// <returns>True if T is registered</returns>  
  99.     public static bool Contains(Type T)  
  100.     {  
  101.       lock (containerLock)  
  102.         return container.ContainsKey(T);  
  103.     }  
  104.   
  105.     /// <summary>  
  106.     /// This is used by the application when it needs an instance of T  
  107.     /// </summary>  
  108.     /// <typeparam name="T">Type that the application needs an instance of</typeparam>  
  109.     /// <returns>An instance of T</returns>  
  110.     public static T GetInstanceOf<T>() where T : class  
  111.     {  
  112.       return GetInstanceOf(typeof (T)) as T;  
  113.     }  
  114.   
  115.     /// <summary>  
  116.     /// This is used by the application when it needs an instance of T  
  117.     /// </summary>  
  118.     /// <param name="T">Type that the application needs an instance of</param>  
  119.     /// <returns>An instance of T</returns>  
  120.     public static object GetInstanceOf(Type T)  
  121.     {  
  122.       lock (containerLock)  
  123.       {  
  124.         Func<object> result;  
  125.         if (container.TryGetValue(T, out result))  
  126.           return result();  
  127.       }  
  128.   
  129.       return OnTypeNotFound != null ? OnTypeNotFound(T) : null;  
  130.     }  
  131.   }  
  132. }  


Features:


  • OnTypeNotFound() - function mapping a type to an object if there is no explicit mapping for it. By default it will throw an exception, but you can replace it if you want, for example, to automatically Mock all types not explicitly mapped.

  • Discover() - this will inspect the calling assembly and automatically register as implementations all types decorated with the Register attribute.

  • Register() - maps a type (normally an interface type, but that's not required) to a Func<> returning an instance of that type. This allows, for example, a singleton implementation where the Func<> always returns the same object. Register() will throw if the type was already registered to avoid overwriting a registration by mistake; for intentional overwriting, use Unregister() first.

  • Unregister() - removes an existing mapping; does not throw even if one does not exist.

  • Contains() - can be used to verify that a type is registered, as GetInstanceOf() will by default throw if it's not.

  • GetInstanceOf() - this finds the function mapped to the given type and executes it, returning its result; if a mapping for this type is not found, GetInstanceOf() returns OnTypeNotFound() - which implicitly throws but can be overwritten.



The automatic registration (the Discover() method) needs implementation classes to be decorated with the Register attribute:


  1. using System;  
  2.   
  3. namespace Helper  
  4. {  
  5.   /// <summary>  
  6.   /// Use this attribute to decorate classes that should be registered as implementations of the argument   
  7.   /// </summary>  
  8.   /// <example>[Register(IRepository)]</example>  
  9.   [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]  
  10.   public class RegisterAttribute : Attribute  
  11.   {  
  12.     public Type ImplementedType { getset; }  
  13.   
  14.     public RegisterAttribute()  
  15.     {  
  16.     }  
  17.   
  18.     public RegisterAttribute(Type implementedType)  
  19.     {  
  20.       ImplementedType = implementedType;  
  21.     }  
  22.   }  
  23. }  


Finally, I have also created a helper class for manual registration:


  1. using System;  
  2.   
  3. namespace Helper  
  4. {  
  5.   /// <summary>  
  6.   /// Fluent interface simplifying the registration of implementations  
  7.   /// </summary>  
  8.   /// <typeparam name="T">Type to implement</typeparam>  
  9.   /// <example>Bind&lt;IRepository&gt;.To(() => new Repository());</example>  
  10.   public static class Bind<T> where T : class  
  11.   {  
  12.     /// <summary>  
  13.     /// Maps T to Func&lt;T&gt;  
  14.     /// </summary>  
  15.     /// <param name="func">Function returning an instance of T</param>  
  16.     public static void To(Func<T> func)  
  17.     {  
  18.       ServiceLocator.Register(func);  
  19.     }  
  20.   }  
  21. }  


Decorating a class is obvious:

  1. [Register(typeof (DataContext))]  
  2. public partial class DBDataContext  
  3. {  
  4. }  


In the initial part of the application (like Global.asax.cs) you can do either this:

  1. ServiceLocator.Discover();  


or this:

  1. Bind<DataContext>.To(() => new DBDataContext());  
  2. Bind<IProductRepository>.To(() => new ProductRepository());  
  3. Bind<IImageRepository>.To(() => new ImageRepository());  


Finally (!), replace all "new class()" with:

  1. var repository = ServiceLocator.GetInstanceOf<IProductRepository>();  


In fact, it is recommended that you do not "hide" the dependencies, which means creating (usually) two constructors: one explicitly indicating dependencies and a default one calling the first, like this:

  1. public class ProductController : Controller  
  2. {  
  3.   private readonly IProductRepository repository;  
  4.   
  5.   public ProductController() : this(ServiceLocator.GetInstanceOf<IProductRepository>())  
  6.   {  
  7.   }  
  8.   
  9.   public ProductController(IProductRepository repository)  
  10.   {  
  11.     this.repository = repository;  
  12.   }  
  13. }  


Whew :) Please let me know if this helps, or if you find any problems with it :)

No comments: