I've created a small, strongly-typed generic Service Locator object for .NET 3.5.
It supports optional polymorphic service location and keyed services. I am looking for a general review, but especially looking for pointers on best practices I've missed or any awkwardness in the API.
This code can also be found as a public GitHub Repository
/// <summary>
/// Simple service locator
/// </summary>
public class ServiceLocator
{
#region Private Classes
/// <summary>
/// Simple .NET 3.5 Tuple class.
/// </summary>
/// <typeparam name="T">The type of the first item.</typeparam>
/// <typeparam name="V">The type of the second item.</typeparam>
[DebuggerDisplay("Item1: {Item1}, Item2: {Item2}")]
private class Tuple<T, V>
{
#region Public Properties
public T Item1 { get; set; }
public V Item2 { get; set; }
#endregion Public Properties
#region Public Constructors
public Tuple(T item1, V item2)
{
Item1 = item1;
Item2 = item2;
}
#endregion Public Constructors
#region Public Methods
/// <summary>
/// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
/// </summary>
/// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
/// <returns>
/// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
/// </returns>
public override bool Equals(object obj)
{
var objAsTuple = obj as Tuple<T, V>;
return objAsTuple != null && IsEqualTo(objAsTuple.Item1, Item1) && IsEqualTo(objAsTuple.Item2, Item2);
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// </returns>
public override int GetHashCode()
{
unchecked
{
var item1HashCode = Item1 == null ? 0 : Item1.GetHashCode();
var item2HashCode = Item2 == null ? 0 : Item2.GetHashCode();
return item1HashCode + item2HashCode;
}
}
#endregion Public Methods
#region Private Methods
/// <summary>
/// Determines whether the first object is equal to the second object, including if they are both null.
/// </summary>
/// <param name="first">The first.</param>
/// <param name="second">The second.</param>
/// <returns>True if both first and second are null or first is equal to second.</returns>
private bool IsEqualTo(object first, object second)
{
return first == null ? second == null : first.Equals(second);
}
#endregion Private Methods
}
#endregion Private Classes
#region Private Fields
/// <summary>
/// The instance for the Singleton pattern.
/// </summary>
private static ServiceLocator instance;
/// <summary>
/// The services
/// </summary>
private IDictionary<Tuple<Type, object>, object> services = new Dictionary<Tuple<Type, object>, object>();
#endregion Private Fields
#region Public Properties
/// <summary>
/// Gets or sets a value indicating whether this <see cref="ServiceLocator"/> is explicit.
/// </summary>
/// <remarks>
/// This property controls how the ServiceLocator handles not being able to find a service of the requested type.
/// If explicit is set to false, the ServiceLocator will look for services that can be casted to the requested type.
/// If explicit is set to true, the ServiceLocator while throw an exception if it cannot find an exact match to the requested service.
/// </remarks>
/// <value>
/// <c>true</c> if explicit; otherwise, <c>false</c>.
/// </value>
public static bool Explicit { get; set; }
#endregion Public Properties
#region Private Properties
private static ServiceLocator Instance
{
get
{
if (instance == null)
{
instance = new ServiceLocator();
}
return instance;
}
}
#endregion Private Properties
#region Public Methods
/// <summary>
/// Clears this instance by removing all services.
/// </summary>
public static void Clear()
{
ServiceLocator.Instance.services.Clear();
}
/// <summary>
/// Gets a service of type T.
/// </summary>
/// <typeparam name="T">The type of service to get.</typeparam>
/// <returns>The instance of type T registered with the Service Locator.</returns>
/// <exception cref="KeyNotFoundException">Thrown when the Service Locator cannot find an un-keyed service of the requested type and Explicit is true, or when the Service Locator cannot find an un-keyed service of the requested type or any type that can be assigned to the requested type and Explicit is false.</exception>
public static T GetService<T>() where T : class
{
return GetService<T>(null);
}
/// <summary>
/// Gets the service using a key object.
/// </summary>
/// <typeparam name="T">The type of service to get.</typeparam>
/// <param name="key">The key to filter multiple services of the same type by.</param>
/// <returns>The instance of type T registered with the provided key in the Service Locator.</returns>
/// <exception cref="System.KeyNotFoundException">Thrown when the Service Locator cannot find an a service of the requested type with a matching key and Explicit is true, or when the Service Locator cannot find a service of the requested type or any type that can be assigned to the requested type with the matching key and Explicit is false.</exception>
public static T GetService<T>(object key) where T : class
{
var type = typeof(T);
var dictKey = new Tuple<Type, object>(type, key);
if (!Instance.services.ContainsKey(dictKey))
{
if (!Explicit)
{
var subTypeKey = Instance.services.Keys.FirstOrDefault(x =>
(key == null ? x.Item2 == null : key.Equals(x.Item2))
&& type.IsAssignableFrom(x.Item1));
if (subTypeKey != null)
{
return (T)Instance.services[subTypeKey];
}
}
throw new KeyNotFoundException(string.Format("Cannot get a value for type: {0} and key: {1}. That type has not been registered yet.", type, key));
}
return (T)Instance.services[dictKey];
}
/// <summary>
/// Registers the specified service with no key.
/// </summary>
/// <typeparam name="T">The type of the service.</typeparam>
/// <param name="service">The service.</param>
/// <exception cref="InvalidOperationException">>Thrown when a service of this type has already been registered without a key.</exception>
public static void Register<T>(T service) where T : class
{
Register(service, null);
}
/// <summary>
/// Registers the specified service with a key.
/// </summary>
/// <typeparam name="T">The type of service.</typeparam>
/// <param name="service">The service.</param>
/// <param name="key">The key.</param>
/// <exception cref="System.InvalidOperationException">Thrown when a service of this type has already been registered with this key.</exception>
public static void Register<T>(T service, object key) where T : class
{
var dictKey = new Tuple<Type, object>(typeof(T), key);
if (Instance.services.ContainsKey(dictKey))
{
throw new InvalidOperationException(string.Format("Cannot register an item of type: {0} and key: {1}. That type is already registered.", dictKey.Item1, key));
}
else
{
Instance.services.Add(dictKey, service);
}
}
/// <summary>
/// Unregisters the specified un-keyed service.
/// </summary>
/// <typeparam name="T">The type of service.</typeparam>
/// <param name="service">The service.</param>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">Thrown when the given service and key are not registered to this ServiceLocator.</exception>
public static void Unregister<T>(T service) where T : class
{
Unregister(service, null);
}
/// <summary>
/// Unregisters the specified keyed service.
/// </summary>
/// <typeparam name="T">The type of service.</typeparam>
/// <param name="service">The service.</param>
/// <param name="key">The key.</param>
/// <exception cref="System.Collections.Generic.KeyNotFoundException">Thrown when the given service and key are not registered to this ServiceLocator.</exception>
public static void Unregister<T>(T service, object key) where T : class
{
var type = typeof(T);
var dictKey = new Tuple<Type, object>(type, key);
if (!Instance.services.ContainsKey(dictKey))
{
throw new KeyNotFoundException(string.Format("Could not find a service of type {0} with key {1}", type, key));
}
Instance.services.Remove(dictKey);
}
#endregion Public Methods
}
3 Answers 3
Regions
Please read are-regions-an-antipattern-or-code-smell
Is there a good use for regions?
No. There was a legacy use: generated code. Still, code generation tools just have to use partial classes instead. If C# has regions support, it's mostly because this legacy use, and because now that too many people used regions in their code, it would be impossible to remove them without breaking existent codebases.
Think about it as about goto. The fact that the language or the IDE supports a feature doesn't mean that it should be used daily. StyleCop SA1124 rule is clear: you should not use regions. Never.
Instead of using the Dictionary.ContainsKey()
method you should better use TryGetValue()
.
Please see: what-is-more-efficient-dictionary-trygetvalue-or-containskeyitem
TryGetValue will be faster.
ContainsKey uses the same check as TryGetValue, which internally refers to the actual entry location. The Item property actually has nearly identical code functionality as TryGetValue, except that it will throw an exception instead of returning false.
Using ContainsKey followed by the item basically duplicates the lookup functionality, which is the bulk of the computation in this case.
In the Unregiser()
method
public static void Unregister<T>(T service, object key) where T : class { var type = typeof(T); var dictKey = new Tuple<Type, object>(type, key); if (!Instance.services.ContainsKey(dictKey)) { throw new KeyNotFoundException(string.Format("Could not find a service of type {0} with key {1}", type, key)); } Instance.services.Remove(dictKey); }
you should remove the ContainsKey()
call and use the returned value of the Remove()
call instead like so
public static void Unregister<T>(T service, object key) where T : class
{
var type = typeof(T);
var dictKey = new Tuple<Type, object>(type, key);
if (Instance.services.Remove(dictKey)) { return; }
throw new KeyNotFoundException(string.Format("Could not find a service of type {0} with key {1}", type, key));
}
Conclusion
Setting aside the points mentioned above:
your xml documentations looks pretty good, I couldn't find any flaw in them (quick glance).
your code is well structured and readable
Some quick remarks:
Remove the regions. They're an anti-pattern basically, and you certainly shouldn't need them with code that's less than 250 lines long.
Your singleton implementation is not liked by Jon Skeet.
Don't use
ContainsKey
. If you need to get something from anIDictionary
, useTryGetValue
instead.
So GetService
would become this:
public static T GetService<T>(object key) where T : class
{
var type = typeof(T);
var dictKey = new Tuple<Type, object>(type, key);
object returnValue;
if(Instance.services.TryGetValue(dictKey, out returnValue))
{
return (T)returnValue;
}
if (!Explicit)
{
var subTypeKey = Instance.services.Keys.FirstOrDefault(x =>
(key == null ? x.Item2 == null : key.Equals(x.Item2))
&& type.IsAssignableFrom(x.Item1));
if (subTypeKey != null)
{
return (T)Instance.services[subTypeKey];
}
}
throw new KeyNotFoundException(string.Format("Cannot get a value for type: {0} and key: {1}. That type has not been registered yet.", type, key));
}
-
1\$\begingroup\$ Thanks for the advice! I hate regions too, but thought I might have been in the minority since everybody at my workplace loves them. Since I seem to (gladly) be wrong, I'll remove them from the repository now. \$\endgroup\$Nick Udell– Nick Udell2015年06月17日 14:37:40 +00:00Commented Jun 17, 2015 at 14:37
The other answers are spot-on regarding the operation of the service locator itself. I'd like to take a moment to regard the Tuple
class. Based on its usage, it should be made immutable. That, and, it should be interface-based for ease of unit testing of the service locator independent of anything else. So let's do so:
private interface ITuple<T, V>
{
T Item1 { get; }
V Item2 { get; }
}
/// <summary>
/// Simple .NET 3.5 Tuple class.
/// </summary>
/// <typeparam name="T">The type of the first item.</typeparam>
/// <typeparam name="V">The type of the second item.</typeparam>
[DebuggerDisplay("Item1: {Item1}, Item2: {Item2}")]
private sealed class Tuple<T, V> : ITuple<T, V>
{
private readonly T _Item1;
private readonly V _Item2;
public Tuple(T item1, V item2)
{
this._Item1 = item1;
this._Item2 = item2;
}
public T Item1
{
get
{
return this._Item1;
}
}
public V Item2
{
get
{
return this._Item2;
}
}
/// <summary>
/// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
/// </summary>
/// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
/// <returns>
/// <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
/// </returns>
public override bool Equals(object obj)
{
var objAsTuple = obj as Tuple<T, V>;
return objAsTuple != null && IsEqualTo(objAsTuple.Item1, this._Item1) && IsEqualTo(objAsTuple.Item2, this._Item2);
}
/// <summary>
/// Returns a hash code for this instance.
/// </summary>
/// <returns>
/// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
/// </returns>
public override int GetHashCode()
{
unchecked
{
var item1HashCode = this._Item1 == null ? 0 : this._Item1.GetHashCode();
var item2HashCode = this._Item2 == null ? 0 : this._Item2.GetHashCode();
return item1HashCode + item2HashCode;
}
}
/// <summary>
/// Determines whether the first object is equal to the second object, including if they are both null.
/// </summary>
/// <param name="first">The first.</param>
/// <param name="second">The second.</param>
/// <returns>True if both first and second are null or first is equal to second.</returns>
private static bool IsEqualTo(object first, object second)
{
return first == null ? second == null : first.Equals(second);
}
}
Let's also create a factory class which returns new instances of the tuple.
private static class Tuple
{
public static ITuple<T, V> Create<T, V>(T item1, V item2)
{
return new Tuple<T, V>(item1, item2);
}
}
Now, use ITuple<,>
in your code where you currently use Tuple<,>
and instead of new Tuple<Type, object>(...)
you'd use Tuple.Create(...)
.
-
\$\begingroup\$ Very good idea. \$\endgroup\$Heslacher– Heslacher2015年06月17日 19:27:46 +00:00Commented Jun 17, 2015 at 19:27
-
1\$\begingroup\$ I'm unsure how much extracting an interface for my
Tuple
implementation helps with regards to unit testing here.Tuple
is used entirely internally (and hidden away from Service Locator users). There is no method for accessing or changing the internalTuple
representation, and I am certain such a method would be TMI for users. As such I'm not even sure how I'd mock it, let alone why I'd want to. Great point on immutability though! \$\endgroup\$Nick Udell– Nick Udell2015年06月17日 22:06:33 +00:00Commented Jun 17, 2015 at 22:06