DEV Community

Cover image for Episode 005 - Dependency Injection - ASP.NET Core: From 0 to overkill
João Antunes
João Antunes

Posted on • Originally published at blog.codingmilitia.com on

Episode 005 - Dependency Injection - ASP.NET Core: From 0 to overkill

In this post/video we continue our look into base concepts of ASP.NET Core, in preparation to what’s in our way for building the application. This time, dependency injection, which was built right into the core of ASP.NET Core (see what I did there? 😆).

For the walk-through you can check the next video, but if you prefer a quick read, skip to the written synthesis.

The playlist for the whole series is here.

Intro

In the last episode, we started looking into some important core concepts when developing ASP.NET Core applications, namely the Program and Startup classes, taking also a quick look into dependency injection and middlewares. In this episode, we go a little further with dependency injection.

Extracting logic from the controller

Why

Normally we don’t want our controllers to have too much logic, particularly business logic. Ideally the controllers focus on MVC/HTTP specific logic - receiving client inputs, pass them along to something responsible for the business logic, return the results back (feeding views or in other forms, such as a JSON payload), maybe including different HTTP status codes depending on the output of the business logic.

This, of course, isn’t mandatory, and I’ve seen articles and talks arguing that for instance, if we’re developing really small microservices, we might as well bypass splitting the code this way, as if adjustments are needed we could just rewrite the service. Anyway, I’ll go with the more classic approach of splitting things, just wanted to make the note that this isn’t set in stone (as nothing is really, there are always multiple ways of doing things).

Some benefits of having the business logic separated from the web framework are the ability to test it without the complexity added by the web framework specificities, as well as allowing the use of the same logic components with different “gateways” (an alternative web framework, a desktop application, etc).

New class libraries

To extract the business logic from the controller, we’ll go along with the old school 3-tier architecture, where our MVC application will be the presentation layer and the extracted logic will make up the business layer. No data layer yet, we’ll get to it in the future.

N-tier architectures may not be what’s hot right now, but for what we’re doing right now, it fits our needs and is simple enough.

Starting with the creation of couple of class libraries CodingMilitia.PlayBall.GroupManagement.Business and CodingMilitia.PlayBall.GroupManagement.Business.Impl. The first library will contain the contracts/API that the clients of the business logic need to know, while the latter will contain the implementation of said logic.

In the contracts library, we need the models (Group class only in this case) and the IGroupService interface.

public class Group
{
    public long Id { get; set; }
    public string Name { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
public interface IGroupsService
{
    IReadOnlyCollection<Group> GetAll();

    Group GetById(long id);

    Group Update(Group group);

    Group Add(Group group);
}

Enter fullscreen mode Exit fullscreen mode

Going to the service implementation library, we create a InMemoryGroupsService class that implements the IGroupsService interface - there isn’t much business logic right now, it’s mostly CRUD, but let’s pretend there is 😛

public class InMemoryGroupsService : IGroupsService
{
    private readonly List<Group> _groups = new List<Group>();
    private long _currentId = 0;

    public IReadOnlyCollection<Group> GetAll()
    {
        return _groups.AsReadOnly();
    }

    public Group GetById(long id)
    {
        return _groups.SingleOrDefault(g => g.Id == id);
    }

    public Group Update(Group group)
    {
        var toUpdate = _groups.SingleOrDefault(g => g.Id == group.Id);

        if (toUpdate == null)
        {
            return null;
        }

        toUpdate.Name = group.Name;
        return toUpdate;
    }

    public Group Add(Group group)
    {
        group.Id = ++_currentId;
        _groups.Add(group);
        return group;
    }
}

Enter fullscreen mode Exit fullscreen mode

As the name implies, the data is stored in memory (it’s not even thread safe) so to say it’s far from production ready is an understatement. It’s exactly the same logic we had in the controller, just pulled into a class of its own.

Heading back to the web application project, we add the references to the newly created libraries and rework the GroupsController to use the IGroupsService instead of having the logic implemented in it.

[Route("groups")]
public class GroupsController : Controller
{
    private readonly IGroupsService _groupsService;

    public GroupsController(IGroupsService groupsService)
    {
        _groupsService = groupsService;
    }

    [HttpGet]
    [Route("")]
    public IActionResult Index()
    {
        return View(_groupsService.GetAll().ToViewModel());
    }

    [HttpGet]
    [Route("{id}")]
    public IActionResult Details(long id)
    {
        var group = _groupsService.GetById(id);

        if (group == null)
        {
            return NotFound();
        }

        return View(group.ToViewModel());
    }

    [HttpPost]
    [Route("{id}")]
    [ValidateAntiForgeryToken]
    public IActionResult Edit(long id, GroupViewModel model)
    {
        var group = _groupsService.Update(model.ToServiceModel());

        if (group == null)
        {
            return NotFound();
        }

        return RedirectToAction("Index");
    }

    [HttpGet]
    [Route("create")]
    public IActionResult Create()
    {
        return View();
    }

    [HttpPost]
    [Route("")]
    [ValidateAntiForgeryToken]
    public IActionResult CreateReally(GroupViewModel model)
    {
        _groupsService.Add(model.ToServiceModel());

        return RedirectToAction("Index");
    }
}

Enter fullscreen mode Exit fullscreen mode

A couple of things to notice:

  • We’re now receiving the IGroupsService in the constructor, so we can use it in the actions
  • ToViewModel and ToServiceModel extension methods - as the presentation layer and the business layer use a different set of models, we need to convert them when passing them along
public static class GroupMappings
{
    public static GroupViewModel ToViewModel(this Group model)
    {
        return model != null ? new GroupViewModel { Id = model.Id, Name = model.Name } : null;
    }

    public static Group ToServiceModel(this GroupViewModel model)
    {
        return model != null ? new Group { Id = model.Id, Name = model.Name } : null;
    }

    public static IReadOnlyCollection<GroupViewModel> ToViewModel(this IReadOnlyCollection<Group> models)
    {
        if (models.Count == 0)
        {
            return Array.Empty<GroupViewModel>();
        }

        var groups = new GroupViewModel[models.Count];
        var i = 0;
        foreach (var model in models)
        {
            groups[i] = model.ToViewModel();
            ++i;
        }

        return new ReadOnlyCollection<GroupViewModel>(groups);
    }
}

Enter fullscreen mode Exit fullscreen mode

We could do this mapping directly in the controller, but it would end up being polluted by this boilerplate code. We could also use AutoMapper to do this auto-magically for us, but it can sometimes hide problems in our code (for instance, we can’t be sure of the existence of references to properties when using it). All in all, I like this extension method idea by a colleague of mine, as it provides nice readability in the controller, so I’ll go with it.

If we try to run this now we’ll get an error, because we’re now expecting to get an IGroupsService in the controller, but we haven’t told the framework how to get it. To do this we need to register the service implementation in the dependency injection container.

Using the builtin container

Registering the service

In the previous episode we saw how to register services in the builtin dependency injection container, and this will be no different. We want to register the InMemoryGroupsService and like we also saw previously, we want it to have lifestyle of type singleton, so the data it keeps in memory sticks around for the lifetime of the application.

To do this, we need a single new line added to the Startup class: services.AddSingleton<IGroupsService, InMemoryGroupsService>();.

Now we can run the application and it behaves has before, just with a different internal organization.

Improve organization

For now, we don’t have too much to worry, as we only have a couple of lines in the ConfigureServices method of the Startup class, but what about when we add more and more?

One good way of keeping the ConfigureServices tidy is to create extension methods on IServiceCollection to register groups of services that go along together. With this in mind, in a newly created IoC folder, we add a new class ServiceCollectionExtensions.

namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceCollectionExtensions
    {
        public static IServiceCollection AddBusiness(this IServiceCollection services)
        {
            services.AddSingleton<IGroupsService, InMemoryGroupsService>();

            //more business services...

            return services;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Notice the namespace “trick” in there, using the same namespace as IServiceCollection. This is something usually done to ease discoverability of extension methods, as it allows for us to go to the ConfigureServices method, type services. and get immediately the methods in intellisense without the need to add another using.

Now in the ConfigureServices we can replace the line we added previously with services.AddBusiness();.

Using third party containers

It’s good that ASP.NET Core comes with dependency injection built right into it, but we need to keep in mind that the out of the box container was built to answer ASP.NET Core internal needs, not every need of the applications built on top of it.

For more advanced scenarios we might have, we can use third party DI containers that can provide those features.

Just to check out this possibility, we’ll add Autofac to the project and play a little with it. The objective is not to go into much detail on Autofac itself, but see how to use it with ASP.NET Core and an example of extra features it adds.

Replacing the container

First thing is to add a couple of NuGet packages - Autofac and Autofac.Extensions.DependencyInjection.

To register dependencies with Autofac, we create a new class AutofacModule (in the IoC folder) that inherits from Module and we override the Load method.

public class AutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder
            .RegisterType<InMemoryGroupsService>()
            .As<IGroupsService>()
            .SingleInstance();
    }
}

Enter fullscreen mode Exit fullscreen mode

The code seen above is the equivalent of what we had in AddBusiness, but adapted to Autofac’s API. Now we need to configure Autofac as the container.

//...
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    //if using default DI container, uncomment
    //services.AddBusiness();

    // Add Autofac
    var containerBuilder = new ContainerBuilder();
    containerBuilder.RegisterModule<AutofacModule>();
    containerBuilder.Populate(services);
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
}
//...

Enter fullscreen mode Exit fullscreen mode

The first change to notice is the ConfigureServices method is no longer void, it now returns an IServiceProvider. This is needed so ASP.NET Core uses the returned service provider instead of building its own out of the IServiceCollection that’s received as an argument.

The Autofac configuration code by the end of the method is a plain copy paste from Microsoft’s dependency injection docs 😇

Basically, what we’re doing here is:

  • Creating an Autofac container builder
  • Registering the module we created with our dependency configurations
  • Populating Autofac’s container with the registrations present in the IServiceCollection, put there by ASP.NET Core’s components
  • Building the container
  • Return a new service provider, implemented using the Autofac container

Again, rerunning the application, we maintain the same functionality.

Using other container features

So far we haven’t used any specific feature of Autofac that justifies the change, unless it was a matter of performance (which wouldn’t be the case, at least at the time of writing, the builtin container seems faster, see here).

Just for arguments sake, let’s use an extra feature Autofac provides: decorators.

public class AutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder
            .RegisterType<InMemoryGroupsService>()
            .Named<IGroupsService>("groupsService")
            .SingleInstance();
        builder
            .RegisterDecorator<IGroupsService>(
            (context, service) => new GroupsServiceDecorator(service), 
            "groupsService");
    }

    private class GroupsServiceDecorator : IGroupsService
    {
        private readonly IGroupsService _inner;

        public GroupsServiceDecorator(IGroupsService inner)
        {
            _inner = inner;
        }

        public IReadOnlyCollection<Group> GetAll()
        {
            Console.WriteLine($"######### Helloooooo from {nameof(GetAll)} #########");
            return _inner.GetAll();
        }

        public Group GetById(long id)
        {
            Console.WriteLine($"######### Helloooooo from {nameof(GetById)} #########");
            return _inner.GetById(id);
        }

        public Group Update(Group group)
        {
            Console.WriteLine($"######### Helloooooo from {nameof(Update)} #########");
            return _inner.Update(group);
        }

        public Group Add(Group group)
        {
            Console.WriteLine($"######### Helloooooo from {nameof(Add)} #########");
            return _inner.Add(group);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

The decorator implements the IGroupsService interface, writing to the console on every method call, then delegating the real work to another IGroupsService implementation it gets in the constructor.

To register the decorator, we changed the InMemoryGroupsService registration to a named one, and then call RegisterDecorator with a lambda to build the decorator and the name we added to the real implementation’s registration. Can’t say I’m the biggest fan of this API, but that’s what we have right now.

Now if we rerun the application, we keep the same functionality as before, but additionally if we look at the console, intermixed with the logs we see the decorators messages.

Outro

This is it for a quick look at dependency injection in ASP.NET Core. Even though not going very deep it’s good enough to prepare for what’s coming, and we’ll introduce more related concepts as needed along the way.

Using Autofac was good for an introduction on replacing the builtin container, but I’m not entirely sold on its API, I’ll probably replace it in the future.

The source code for this post is here.

Please send any feedback you have, so the next posts/videos can be better and even adjusted to more interesting topics.

Thanks for stopping by, cyaz!

Top comments (11)

Collapse
 
chenge profile image
chenge

DI is just a parameter of method, why so complicated?

Collapse
 
joaofbantunes profile image
João Antunes • Edited

You're right, DI is really a simple concept.

I can't speak from experience, as I never built such a framework, but I would say the complexity comes from building (potentially complex) objects graphs automatically at runtime, given the configuration we provide.

In C# (and for what I've seen of Java) the solutions are very similar to this. Other languages have probably different approaches, maybe they're simpler, can't really say.

Collapse
 
chenge profile image
chenge

I had ever done java and c# projects and know a little DI like spring. Later I select Ruby, when I do rails project I find I need DI to mock test, but no need for DI framework.

Maybe big project need a complicated DI framework like Spring.

Thread Thread
 
joaofbantunes profile image
João Antunes

Yeah, didn't do anything with Ruby myself, so can't make a good comparison.

Maybe we're lucky and there's another DEV member reading this that has experience in both and wants to chime in 🙂.

Thread Thread
 
chenge profile image
chenge

Welcome, happy to talk DI with you. I have the short ruby code to demo:

    class Factory  
      attr_accessor :product  
      def produce  
        @product.new  
      end  
    end  
    class Product  
      #..  
    end  
    fac = Factory.new  
    fac.product = Product  
    fac.produce  
Thread Thread
 
joaofbantunes profile image
João Antunes

Ah, I see, you create a factory and then use it instead of creating the Product directly.

In C# we normally use the dependency injection container as the factory (we still use factories on occasion, but for different reasons).

Using the container as the factory, and it being tightly integrated into the web framework, in the controllers, like you can see above, we just add the dependencies we want in the constructor, and the framework passes them in automatically.

public class GroupsController : Controller
{
    private readonly IGroupsService _groupsService;

    public GroupsController(IGroupsService groupsService)
    {
        _groupsService = groupsService;
    }

    //...
}
Thread Thread
 
chenge profile image
chenge
class PlayController < Controller

  def create(params)
     store = Invoice
     Invoice.generate_invoice(params, store)
     ... 
  end
end


In Ruby code is like this. Don't need a DI container.

Thread Thread
 
joaofbantunes profile image
João Antunes

Yupe. Different ways to get to the same end result of decoupling components 🙂

I like the C# approach, probably because I'm used to it, but I can understand that coming from other languages it seems overly complex.

Being able to just declare the dependencies in the constructor and they'll be there when running is nice (even if a bit magic) and gives quick visibility on the dependencies of a given class just by looking at the constructor. It does come with the hidden complexity you talked about, so as always, there are trade offs.

Collapse
 
dyagzy profile image
dyagzy

Many thanks for this chapter, see questions below:

  1. How do I when to use a 3rd party DI or IoC in my project

  2. Why did you use an extension method for ToViewModel and ToServiceModel? Although it seems a little confusing to me but I will go over it again till am able to understand it's use case.

  3. Which other way can be use to achieve the use of the extension method as you have done without using an extension method?

Thank you for your reply always.

Collapse
 
joaofbantunes profile image
João Antunes

Hi there!
Let's see if I can answer all of them:

  1. If the built-in container is good enough for your needs, probably just stick with it. Otherwise, you can look at alternatives if:
    • You already have experience and like a different container
    • You need some features that another container provides (including performance)
  2. These extension methods are just to move the boilerplate mapping code out of the controller. I could just have mapped everything in there, but the controller code would be polluted with things that are not really relevant for its logic.
  3. Not sure I understand this one. An extension method is a normal static method, with syntactic sugar to make things more readable, so instead of doing GroupMappings.ToViewModel(group), we can do group.ToViewModel(). Was this the question, or another thing?
Collapse
 
dyagzy profile image
dyagzy

Thank you for your responses.
I quite understand your answers to my questions (1 and 2).

I guess I should follow up with your tutorial to the next episode perhaps I will better understand your answer to the 3rd question.

I will revert back to you with more questions.