ASP.NET 6 and Dependency Injection


From the beginning of the .NET Core era, Dependency Injection is to be used thoroughly. Controllers, Business Logic, and just about any other dependency can use Dependency Injection. So, what is it and how is it set up? (The source code for this project can be found on Github here.)

What is Dependency Injection

Dependency Injection is basically providing an object all of the classes/interfaces that it needs rather than having the object create them itself. The biggest benefit of Dependency Injection is testability. Basically, if an object depends on a bunch of interfaces, and they’re injected (i.e. in the constructor) then these interfaces can be easily mocked for testing purposes. Also, this can lead to cleaner code.

How to setup Dependency Injection in ASP.NET 6

I created a new ASP.NET 6 project, and I modified the Program.cs file like this:

using DepInjExample.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddTransient<IService1, Service1>();
builder.Services.AddScoped<IService2, Service2>();
builder.Services.AddSingleton<IService3, Service3>();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

On lines 6-8 I’m adding Service1, Service2 and Service3 as interfaces (IService1, 2, and 3) that they implement. Notice that they’re added as Transient, Scoped, and Singleton respectively. Let’s talk about what that means.

Transient: This means that every time the class (or interface) is referenced, there will be a new instance generated. If it’s referenced 3 times, then there will be 3 new instances generated for those.

Scoped: This means that services are created once per client request. So if a service is referenced more than once in one request, there will be only one instance of that service.

Singleton: This means that there will be only one instance of this object. No matter how many times it’s referenced, they will all refer to that one instance. NOTE: This can lead to threading errors if not used properly.

There are some rules for service references based on their lifetimes:

  • A Scoped service can reference the other two service types (transient and singleton).
  • A Transient service can reference a singleton but not a scoped service.
  • A Singleton service can reference a transient service but not a scoped service.

If these rules are broke somewhere, you’ll get an error at runtime, which pretty much sucks and can be tedious to track down. Next, let’s look at the services. NOTE: Service1 and Service2 have the exact same implementation.

namespace DepInjExample.Services
{
    public interface IService1
    {
        int GetValue();
    }

    public class Service1 : IService1
    {
        private int value = 0;

        public int GetValue()
        {
            return value++;
        }
    }
}

Service1 contains an int value and contains one function that simply returns the value and then increments it. It implements the interface, IService1.

Service2 is exactly same as Service1 but the interface name is IService2.

namespace DepInjExample.Services
{
    public interface IService3
    {
        int GetValue();
    }

    public class Service3 : IService3
    {
        private int value = -1;
        public int GetValue()
        {
            Interlocked.Increment(ref value);
            return value;
        }
    }
}

Service3 contains an int and implements the IService3 interface. Unlike the other services, Service3 increments the value in a thread-safe way (in case it’s accessed by more than one thread). This is the “Interlocked.Increment” function. Ok, next, let’s see how we would use the services in a controller.

Using the services in a controller

Here’s the controller code:

using DepInjExample.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace DepInjExample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {
        private readonly IService1 service1;
        private readonly IService2 service2;
        private readonly IService3 service3;

        public TestController(IService1 service1, 
            IService2 service2,
            IService3 service3)
        {
            this.service1 = service1;
            this.service2 = service2;
            this.service3 = service3;
        }

        [HttpGet("one")]
        public IActionResult Test1()
        {
            return Ok(service1.GetValue());
        }

        [HttpGet("two")]
        public IActionResult Test2()
        {
            return Ok(service2.GetValue());
        }

        [HttpGet("three")]
        public IActionResult Test3()
        {
            return Ok(service3.GetValue());
        }
    }
}

In the constructor of the controller are the three service instances. This is where Dependency Injection comes into play. So, the system “injects” the services into the constructor. This is called “constructor injection” and is the only form of injection that the default DI framework supports in ASP.NET 6.

From there, I implement three REST endpoints that call each service respectively. Let’s check out the results. The project uses Swagger, so we can call the endpoints via the Swagger UI while the project is running.

As you can see, the first endpoint will always return 0 since the service is registered as transient.

The second endpoint is also going to always return 0 since it’s only being used in the REST endpoint. As an exercise for the reader, you could create another service that uses Service2 and increments it, then we’d see it go up depending on the number of times we call the GetValue function prior to calling it from the REST endpoint.

Since the third endpoint uses the Singleton service, it will increment as many times as we call the endpoint. Easy peasy…

That’s all I have for today. Thanks for stopping by!

, ,