8 min read

5 Ways Selecting Injected Instance from Multiple Instances of Same Interface on ASP.NET Core

Justin Yoo

While building an ASP.NET Core application, setting an IoC container for dependency injection is nearly inevitable. ASP.NET Core offers a built-in IoC container that is easy to use, without having to rely on any third-party libraries. Throughout this post, I'm going to discuss five different ways to pick up a dependency injected from multiple instances sharing with the same interface.

Implementing Interface and Classes

Let's assume that we're writing one IFeedReader interface and three different classes, BlogFeedReader, PodcastFeedReader and YouTubeFeedReader implementing the interface (line #1, 7, 22, 37). There's nothing fancy.

public interface IFeedReader
{
string Name { get; }
string GetSingleFeedTitle();
}
public class BlogFeedReader : IFeedReader
{
public BlogFeedReader()
{
this.Name = "Blog";
}
public string Name { get; }
public string GetSingleFeedTitle()
{
return "This is blog item 1";
}
}
public class PodcastFeedReader : IFeedReader
{
public PodcastFeedReader()
{
this.Name = "Podcast";
}
public string Name { get; }
public string GetSingleFeedTitle()
{
return "This is audio item 1";
}
}
public class YouTubeFeedReader : IFeedReader
{
public YouTubeFeedReader()
{
this.Name = "YouTube";
}
public string Name { get; }
public string GetSingleFeedTitle()
{
return "This is video item 1";
}
}

IoC Container Registration #1 – Collection

Let's register all the classes declared above into the ConfigureServices() method in Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IFeedReader, BlogFeedReader>();
services.AddTransient<IFeedReader, PodcastFeedReader>();
services.AddTransient<IFeedReader, YouTubeFeedReader>();
}

By doing so, all three classes have been registered as IFeedReader instances. However, from the consumer perspective, it doesn't know which instance is appropriate to use. In this case, using a collection as IEnumerable<IFeedReader> is useful. In other words, inject a dependency of IEnumerable<IFeedReader> to the BlogFeedService class and filter one instance from the collection (line #7).

public class BlogFeedService
{
private readonly IFeedReader _reader;
public BlogFeedService(IEnumerable<IFeedReader> readers)
{
this._reader = readers.SingleOrDefault(p => p.Name == "Blog");
}
public string GetTitle()
{
return this._reader.GetSingleFeedTitle();
}
}

It's one way to use the collection as a dependency. The other way to use the collection is to use a loop (line #12). It's useful when we implement either a Visitor Pattern or Iterator Pattern.

public class BlogFeedService
{
private readonly IEnumerable<IFeedReader> _readers;
public BlogFeedService(IEnumerable<IFeedReader> readers)
{
this._readers = readers;
}
public string GetTitle()
{
foreach (reader in this._readers)
{
if (reader.Name != "Blog")
{
continue;
}
return reader.GetSingleFeedTitle();
}
}
}

IoC Container Registration #2 – Resolver

It's similar to the first approach. This time, let's use a resolver instance to get the dependency we want. First of all, declare both IFeedReaderResolver and FeedReaderResolver (line #1, 6). Keep an eye on the instance of IServiceProvider as a dependency. It's used for the built-in IoC container of ASP.NET Core, which can access to all registered dependencies.

In addition to that, this time, we don't need the Name property any longer as we use conventions to get the required instance (line 17-18).

public interface IFeedReaderResolver
{
IFeedReader Resolve(string name);
}
public class FeedReaderResolver : IFeedReaderResolver
{
private readonly IServiceProvider _provider;
public FeedReaderResolver(IServiceProvider provider)
{
this._provider = provider;
}
public IFeedReader Resolve(string name)
{
var type = Assembly.GetAssembly(typeof(FeedReaderResolver)).GetType($"{name}FeedReader");
var instance = this._provider.GetService(type);
return instance as IFeedReader;
}
}

After that, update ConfigureServices() on Startup.cs again. Unlike the previous approach, we registered xxxFeedReader instances, not IFeedReader. It's fine, though. The resolver sorts this out for BlogFeedService.

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<BlogFeedReader>();
services.AddTransient<PodcastFeedReader>();
services.AddTransient<YouTubeFeedReader>();
services.AddTransient<IFeedReaderResolver, FeedReaderResolver>();
}

Update the BlogFeedService class like below (line #5).

public class BlogFeedService
{
private readonly IFeedReader _reader;
public BlogFeedService(IFeedReaderResolver resolver)
{
this._reader = resolver.Resolve("Blog");
}
public string GetTitle()
{
return this._reader.GetSingleFeedTitle();
}
}

IoC Container Registration #3 – Resolver + Factory Method Pattern

Let's slightly modify the resolver that uses the factory method pattern. After this modification, it removes the dependency on the IServiceProvider instance. Instead, it creates the required instance by using the Activator.CreateInstance() method (line #5-6).

public class FeedReaderResolver : IFeedReaderResolver
{
public IFeedReader Resolve(string name)
{
var type = Assembly.GetAssembly(typeof(FeedReaderResolver)).GetType($"{name}FeedReader");
var instance = Activator.CreateInstance(type);
return instance as IFeedReader;
}
}

If we implement the resolver class in this way, we don't need to register all xxxFeedReader classes to the IoC container, but IFeedReaderResolver would be sufficient. By the way, make sure that all xxxFeedReader instances cannot be used as a singleton if we take this approach.

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IFeedReaderResolver, FeedReaderResolver>();
}

IoC Container Registration #4 – Explicit Delegates

We can replace the resolver with an explicit delegates. Let's have a look at the code below. Within Startup.cs, declare a delegate just outside the Startup class.

public delegate IFeedReader FeedReaderDelegate(string name);

Then, update ConfigureServices() like below. As we only declared the delegate, its actual implementation goes here. The implementation logic is not that different from the previous approach (line #9-10).

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<BlogFeedReader>();
services.AddTransient<PodcastFeedReader>();
services.AddTransient<YouTubeFeedReader>();
services.AddTransient<FeedReaderDelegate>(provider => name =>
{
var type = Assembly.GetAssembly(typeof(FeedReaderResolver)).GetType($"FeedReaders.{name}FeedReader");
var instance = provider.GetService(type);
return instance as IFeedReader;
});
}

Update the BlogFeedService class (line #5). As FeedReaderDelegate returns the IFeedReader instance, another method call should be performed through the method chaining (line #12).

public class BlogFeedService
{
private readonly FeedReaderDelegate _delegate;
public BlogFeedService(FeedReaderDelegate @delegate)
{
this._delegate = @delegate;
}
public string GetTitle()
{
return this._delegate("Blog").GetSingleFeedTitle();
}
}

IoC Container Registration #5 – Implicit Delegates with Lambda Function

Instead of using the explicit delegate, the Lambda function can also be used as the implicit delegate. Let's modify the ConfigureServices() method like below. As there is no declaration of the delegate, define the Lambda function (line #7-13).

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<BlogFeedReader>();
services.AddTransient<PodcastFeedReader>();
services.AddTransient<YouTubeFeedReader>();
services.AddTransient<Func<string, IFeedReader>>(provider => name =>
{
var type = Assembly.GetAssembly(typeof(FeedReaderResolver)).GetType($"FeedReaders.{name}FeedReader");
var instance = provider.GetService(type);
return instance as IFeedReader;
});
}

As the injected object is the Lambda function, BlogFeedService should accept the Lambda function as a dependency (line #5). While it's closely similar to the previous example, it uses the Lambda function this time for dependency injection.

public class BlogFeedService
{
private readonly Func<string, IFeedReader> _func;
public BlogFeedService(Func<string, IFeedReader> func)
{
this._func = func;
}
public string GetTitle()
{
return this._func("Blog").GetSingleFeedTitle();
}
}

So far, we have discussed five different ways to resolve injected dependencies using the same interface in an ASP.NET Core application. Those five approaches are very similar to each other. Then, which one to choose? Well, there's no one approach better than the other four, but I guess it would depend purely on the developer's preference.