zaterdag 26 januari 2013

Autofac and multiple implementations of an interface


How can we use Autofac to inject multiple implementations of an interface into a class, and how can we choose a specific implementation?

Let's say we have this interface for an object with which you can view files:

public interface IViewer
{
   void View(string filename);
}


And we have multiple implementations:

public class PdfViewerLarge : IViewer
{
   public void View(string filename)
   {
      // Do something smart
   }
}

public class PdfViewerSmall : IViewer
{
   public void View(string filename)
   {
      // Do something smart
   }
}


public class XlsViewerLarge : IViewer
{
   public void View(string filename)
   {
      // Do something smart
   }
}


public class XlsViewerSmall : IViewer
{
   public void View(string filename)
   {
      // Do something smart
   }
}

As you can see we have four Viewers. How can we use Autofac to inject these Viewers into a class?
First let's register the Viewers with Autofac, in an MVC3 website:

var builder = new ContainerBuilder();
builder.RegisterControllers(typeof (MvcApplication).Assembly);
builder.RegisterType<PdfViewerBig>().As<IViewer>();
builder.RegisterType<PdfViewerSmall>().As<IViewer>();
builder.RegisterType<XlsViewerSmall>().As<IViewer>();
var container = builder.Build();

Autofac will automatically put all viewers into an IEnumerable<IViewer>, which you can resolve like this:

var viewers = container.Resolve<IEnumerable<IViewer>>();

And we can easily use this in an MVC3 website like this:

public class HomeController : Controller
{
   private readonly IEnumerable<IViewer> _viewers;

   public HomeController(IEnumerable<IViewer> viewers)
   {
   _viewers = viewers;
   }

   public ActionResult Index()
   {
      foreach(var viewer in _viewers)
      {
         viewer.View("filename.pdf");
      }

      return View();
   }
}


How to choose a specific implementation from a collection?

The example above of the HomeController is pretty stupid, as we're using each viewer to view a document, even if the viewer is not suited for the filetype. What we realy want is something like:

var viewer = _viewers.Single(it => it.Filetype == ".pdf");
viewer.View("file.pdf");

There are multiple ways to do this. These are the three I like:
1. Use an enum value with autofac
2. Use Metadata with Autofac
3. Use Metadata within each Viewer


Enum value with Autofac

If you can think of a way to describe every viewer with an enum then this is a good option. It works like this;

Declare an enum:

public enum ViewerType { PdfLarge, PdfSmall, XlsLarge, XlsSmall }

Next register the type using the enum with Autofac:


builder.RegisterType<PdfViewerLarge>()
   .Keyed<IViewer>(ViewerType.PdfLarge);


builder.RegisterType<PdfViewerSmall>()
   .Keyed<IViewer>(ViewerType.PdfSmall);


builder.RegisterType<XlsViewerLarge>()
   .Keyed<IViewer>(ViewerType.XlsLarge);


builder.RegisterType<XlsViewerSmall>()
   .Keyed<IViewer>(ViewerType.XlsSmall);

In an MVC Controller the Viewers will be injected like this:

public class HomeController : Controller
{
   private readonly IIndex<ViewerType, IViewer> _viewers;

   public HomeController(IIndex<ViewerType, IViewer> viewers)

   {
      _viewers = viewers;
   }
}


Now we can use this way to select the desired implementation in an ActionMethod :

public ActionResult Index()
{
   var viewer = _viewers[ViewerType.PdfLarge];

   // do something with viewer

   return View();
}


Metadata with Autofac

Autofac provides a way to add metadata to registered services:

builder.Register(c => new PdfViewerLarge())
   .As<IViewer>()
   .WithMetadata("FileType", ".pdf");

We can also use strong typed metadata. First we have to define which metadatakeys we have:

public interface IViewerMetadata
{
    string FileType { get; }
    long MaxFileSize { get; }
}

Now we can use this metadata to register a type:

builder.RegisterType<XlsViewerSmall>()
   .As<IViewer>()
   .WithMetadata<IViewerMetadata>(m =>
      {
         m.For(p => p.MaxFileSize, 10000);
         m.For(p => p.FileType, ".xls");
       });

In an MVC Controller the Viewers will be injected like this:

public class HomeController : Controller
{
   private IEnumerable<Meta<IViewer, IViewerMetadata>> _viewers;

   public HomeController(
             IEnumerable<Meta<IViewer, IViewerMetadata>> viewers)
   {
      _viewers = viewers;
   }
}

Now we can use this way to select the desired implementation in an ActionMethod :

public ActionResult Index()
{
   var viewer = _viewers.Single(
                   it => it.Metadata.FileType == ".xls" &&
                   it.Metadata.MaxFileSize > 1000
                               ).Value;


   // do something with viewer

   return View();
}



Metadata inside an implementation

Another option to store metadata is inside the implementation itself. This is the way it works:

First we take an implementation, for example the PdfViewerLarge.


public class PdfViewerLarge : IViewer
{
   public void View(string filename)
   {
      // Do something smart
   }
}


We have to add the metadata to it, as public properties. But first add the metadata to the interface:

public interface IViewer
{
   string FileType {get;}
   long MaxFileSize {get;}
   void View(string filename);
}

Next we can implement it:



public class PdfViewerLarge : IViewer
{

   public string FileType
   {
      get { return ".pdf"; }
   }
   
   public long MaxFileSize

   {
      get { return 1000000; }
   }



   public void View(string filename)
   {
      // Do something smart
   }
}

When we register the type in Autofac we don't have to include any metadata:


builder.RegisterType<XlsViewerSmall>()
   .As<IViewer>();

But we can still use metadata in our Actionmethod:


public ActionResult Index()
{

   var viewer = _viewers.Single(
                   it => it.Value.FileType == ".xls" &&
                   it.Value.MaxFileSize > 1000
                                ).Value;


   // do something with viewer

   return View();
}


One drawback of this method is that every type has to be instanciated first. If creating a new instance of a specific implementation is an expensive operation, the other two options are preferable.

2 opmerkingen:

  1. I would consider the Chain of Responsibility pattern. It's a very clean and OO solution to the problem.

    https://refactoring.guru/design-patterns/chain-of-responsibility

    BeantwoordenVerwijderen