Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A tutorial on Service locator pattern with implementation

4.83/5 (21 votes)
3 Jun 2013CPOL7 min read 114.1K   1.8K  
This article provides a tutorial on service locator pattern with a sample implementation

Introduction

This article provides a tutorial on the Service Locator pattern with a sample implementation of a service locator which uses activators to create local or remote services, and discoverers to discover services at run-time.

Background

Fundamentally the locator pattern provides a way of reducing coupling between classes and their dependencies by encapsulating the process involved in obtaining a dependent service.

Let's start with a simple example of a class which uses a logging service implemented by a Logger class:

C#
class ClientClass
{
    private Logger _logger;
    public ClientClass()
    {
         _logger = new Logger();
        _logger.Log("Logger was created");
    }
}

Image 1

In the above snippet:  

  • ClientClass is tightly coupled with a certain implementation of Logger.
  • ClientClass understands how to create an instance of Logger (has a reference to the assembly where Logger is located).
  • To test ClientClass, you need this specific implementation of Logger. Of course, you can Mock Logger.  

There is nothing wrong with ClientClass and its tight coupling with Logger but if you have a need to:

  • Have a loose coupling with Logger so that you can change its implementation with little or no change to ClientClass.
  • Remove repetitive logic to locate and create dependent services.
  • Be independent of the location of the logging service and the way to instantiate the Logger service.

Then you might want to consider the Service Locator pattern. A good read on the Service locator pattern is on MSDN here. Instead of directly jumping to the service locator pattern , we will walk through potential solutions and see if we can arrive at that pattern ourselves.  

A shortcut for COMmers 

If you are familiar with COM then you might want to read this section to get a quick understanding of the service locator pattern. If not, you can skip this section.

Remember the good old days when you could just use CoCreateInstance/Ex to get a reference to a contract implemented by a class. Here is a simplistic pseudo-code of what you could do

CoCreateInstance "class id", "in process or out of process", "contract/interface ID"

You did not have to care about where the actual executable is located on the system (or which system). COM somehow returned an instance of the class you requested which implemented your required contract. Now the question is what happened internally. I will take the example of an in-process component but the paradigm remains the same for out of process or remote components. Note that this is a simplified version of what actually happens.

When you call CoCreateInstance:

  • COM would look-up the location (DLL) where the class which implements the interface you need (In the registry under HKEY_CLASSES_ROOT\CLSID).
  • COM would load the DLL.
  • COM would call DllGetClassObject to create the factory for the class.
  • COM would call CreateInstance on the factory to get an actual instance of the class you requested.
  • COM would return the class you requested.

So client classes never had to worry about the location and the activation of their dependent components. This is exactly the intent of the service locator pattern. You could call ServiceLocator::GetComponent(type, contract) to get an object and somehow the service locator would return an instance of the class you requested. Similar to the COM implementation, the Service locator should use a factory in the background to create the instance of the class.

Moving towards the Service locator pattern

To satisfy some of the needs specified in Background section above, you may refactor your class to look like this:

C#
public class ClientClass
{
    private ILogger _logger;

    public ClientClass()
    {
        _logger = LoggerFactory.GetLogger("context");
        _logger.Log("Logger was created");
    }
}

Image 2

You may even extend your design to register various kinds of loggers with LoggerFactory using configuration files, reflection etc. E.g.,

C#
public class ObjectFactory
{
    public readonly static ObjectFactory=  new ObjectFactory();

    public T GetInstance<T>(string context) where T : class
    {

        if(typeof(T).Equals(typeof(ILogger)))
        {
            return new Logger() as T; //simplistically. Context is not used here in this example
        }
        //other types....
        return null;
    }
}

ClientClass will then use ObjectFactory to get access to various services:

C#
ILogger logger = GeneralObjectCreator.Instance.GetInstance<ILogger>();
ICalc calculator = GeneralObjectCreator.Instance.GetInstance<ICalc>(); 

This is now very similar to an implementation of a Locator pattern. One question which may come up at this time is what is the difference between a Locator and a Factory. Quoting from MSDN here:

The Service Locator pattern does not describe how to instantiate the services. It describes a way to register services and locate them. Typically, the Service Locator pattern is combined with the Factory pattern and/or the Dependency Injection pattern. This combination allows a service locator to create instances of services.  

On the other hand, the Factory pattern is a creational pattern i.e., it is responsible either directly or indirectly to construct related objects.

But there are as many opinions floating out there. You can find implementations of service locators which also are responsible to create instances of services. Our implementation in this article will use the following paradigm:

  • Locator will be primarily responsible to provide a central repository of services.  
  • Locator will use 'activators' to create instances of the actual types. This will decouple the locator from various ways to create instances e.g., create instance from a type in a local assembly or connect to remote service using remoting or WCF.   
  • Services can be added programmatically or through configuration files to the locator.
  • Discoverers can be used to dynamically add discovered services to the locator.

Note that this is an example implementation and is not intended to be used in production environments as is.

Some people may have questions on service locator differs from DI? DI is another pattern which solves the same problem in a different way. The primary difference being that if you use service locator pattern, your classes are explicitly dependent on service locator while DI performs auto-wiring to keep your individual classes independent of service locator (classes are 'given/injected with' dependencies by an 'assembler'). An DI can also act as a locator in some aspects. The locator can be used as a container by DI to find various services to be used during auto-wiring.

Which is better? Service locator or DI? Depends on the context, need and the person answering the question. Both have their advantages and disadvantages and there is enough material on the web to look at to get this information.

Implementation

The diagram below shows a high level view of my implementation of the service locator pattern for the purpose of this tutorial:

Image 3

Let us look at each player above in detail:  

Client

The client uses the service locator to get access to various services.

Here is an simple example of usage of the locator by the client:

C#
IServiceLocator locator = ServiceLocator.instance();
ILogger logger = locator.GetInstance<ILogger>();
//or locator.GetInstance<ILogger>("logger") for named access
logger.Log("this is a log message");

Here is a more extensive example of usage of the locator by the client:

C#
public void TestLocator()
{
    //add manual entry to locator
    LocatorEntry entry = new LocatorEntry();
    entry.ContractTypeName = "LocatorExample.Contracts.ILogger, LocatorExample.Contracts";
    entry.IsSingleton = false;
    entry.LocationName = "ManuallyEnteredLoger";
    entry.ActivatorTypeName = typeof(DefaultAssemblyActivator).AssemblyQualifiedName;
    var activatorConfig = new DefaultAssemblyActivatorConfig();
    activatorConfig.TypeToCreate = "ExampleComponents.Logger, ExampleComponents";
    entry.ActivatorConfig = activatorConfig;
    ServiceLocator.Instance.RegisterLocatorEntry(entry);

    ILogger logger = ServiceLocator.Instance.GetReference<ILogger>("ManuallyEnteredLoger");
    logger.Log("Manually added logger called");

    //get logger for configuration file
    logger = ServiceLocator.Instance.GetReference<ILogger>("Logger");
    logger.Log("Logger added from config file called");

    //Try remoting activator (specified in config file, transparent to client)
    ICalc calc = ServiceLocator.Instance.GetReference<ICalc>("Calculator");
    int sum = calc.Add(10, 20);
    logger.Log(" Result of addition using remoting activator is " + sum);

    //Try dynamically discovered type
    IAuthenticator authenticator = ServiceLocator.Instance.GetReference<IAuthenticator>();
    bool authenticated = authenticator.Authenticate("user", "pass");
    ServiceLocator.Instance.GetReference<ILogger>("Logger").Log(
            "Dynamically discovered Authenticator returned " + authenticated);

    //Get all services which implement ILogger
    IList<ILogger> loggers = ServiceLocator.Instance.GetAllReferences<ILogger>();
    foreach (var item in loggers)
    {
        item.Log("Calling a service which impements ILogger");
    }

    //make sure entries which are marked as singletons are singletons
    ILogger logger1 = ServiceLocator.Instance.GetReference<ILogger>("SingletonLogger");
    ILogger logger2 = ServiceLocator.Instance.GetReference<ILogger>("SingletonLogger");
    if( object.ReferenceEquals(logger1, logger2))
    {
        logger.Log("Got the same reference for SingletonLogger");   
    }

    //try with non singletons
    logger1 = ServiceLocator.Instance.GetReference<ILogger>("Logger");
    logger2 = ServiceLocator.Instance.GetReference<ILogger>("Logger");
    if (! object.ReferenceEquals(logger1, logger2))
    {
        logger.Log("Got different references for non Singletons");
    }
}

The configuration file for above example looks like:

XML
<configuration>
  <configSections>
    <section name="Locator" type="Locator.ServiceLocatorConfig, Locator" />
  </configSections>

  <Locator>
    <LocatorEntries>
      <!--Add a locator entry for a  logger using activation from assembly-->
      <Add LocationName="Logger" 
           ContractTypeName="LocatorExample.Contracts.ILogger, LocatorExample.Contracts"
           ActivatorTypeName="Locator.DefaultAssemblyActivator, Locator"
           ActivatorConfigType="Locator.DefaultAssemblyActivatorConfig, Locator">
        <ActivatorConfig TypeToCreate="ExampleComponents.Logger, ExampleComponents" />

      </Add>

      <!--Add another locatory entry for a advanced logger using activation from assembly-->
      <Add LocationName="SingletonLogger" 
           ContractTypeName="LocatorExample.Contracts.ILogger, LocatorExample.Contracts"
           ActivatorTypeName="Locator.DefaultAssemblyActivator, Locator"
           ActivatorConfigType="Locator.DefaultAssemblyActivatorConfig, Locator"
           IsSingleton="true">
        <ActivatorConfig TypeToCreate="ExampleComponents.AdvancedLogger, ExampleComponents" />

      </Add>

      <!--Add another locator entry for a calculator using remoting activation. -->
      <Add LocationName="Calculator" ContractTypeName="LocatorTest.ICalc, LocatorTest"
           ActivatorTypeName="Activators.RemotingServiceActivator, Activators"
           ActivatorConfigType="Activators.RemotingActivatorConfig, Activators">
        <ActivatorConfig Uri="http://localhost:9871/Calc" />

      </Add>
    </LocatorEntries>

    <Discoverers>
      <!--Dynamically discover IAuthenticator types-->
      <Add DiscovererTypeName="Locator.DefaultAssemblyDiscoverer, Locator"
           DiscovererConfigType="Locator.DefaultAssemblyDiscovererConfig, Locator">
        <DiscovererConfig SearchPath=".;c:\temp;plugins;..\plugins"
          SearchForContract="LocatorExample.Contracts.IAuthenticator, LocatorExample.Contracts" />
      </Add>
    </Discoverers>
  </Locator>
</configuration>

Locator

The locator is responsible to keep a registry of services which can be requested by clients. These services can be accessed by either name or the contract the service implements, e.g., Locator.GetInstance(ILogger.GetType()) or Locator.GetInstance("name"). In addition other facilities can be provided by the locator like maintaining shared instances of services (singletons) etc. 

The locator does not activate the services itself but uses activators to do so. This decouples the locator from various ways to create instances of services e.g., create an instance of a service from a local assembly or connect to a remote service using remoting or WCF.    

The information required by the locator and the activators to activate a type are:

  • LocatorName: This allows clients to access components by name. Optional.
  • ContractType: This allows the clients to access components by type.
  • IsSingleton: This specifies if the locator should return a shared instance vs. a new instance.
  • ActivatorTypeName: This is the activator which is used by the locator to activate a type
  • ActivatorConfigType: This is the activator specific configuration object used by an activator e.g., this may be the fully qualified name of a C# class e.g., Utils.Logger, logger or a Uri of a remote object. The locator provides this information to the Activator. Note that I have used a dynamic configuration section to keep the configuration of the activator with the locator entry.

Discoverer

As demonstrated in above examples, it is possible to register services with locator programmatically or through configuration file. In order to register types not known at deployment time, one can use the discovery paradigm to register types with the locator. The example I have provided searches assemblies in the provided path list. Additional discoverers can be created to retrieve list of services from central repository.

The following diagram shows a sequence diagram of overall workflow (simplistically).

Image 4

Contents of attached solution

The attached solution consists of the following projects:

  1. Locator: This is the main project containing the implementation of the Service locator along with DefaultAssemblyActivator and DefaultAssemblyDiscoverer.
  2. Activators: Contains an example of a remoting activator.
  3. ExampleComponents: Contains simple implementations of services like logger and authenticator.
  4. LocatorExample.Contracts: Contains the contracts for logger and authenticator (ILogger, IAuthenticator).
  5. Locator.Utilities: Contains some utility classes used by locator.
  6. LocatorTest: Contains example of usage of the locator in addition to implementation of a sample calculator service.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)