BLOG
25 October 2022

Dapr part 5: Dependency Injection and Better Code Organization

tech

This is the fifth article in the Dapr series. The previous one can be found here. If you missed it, please check it out.  

In this case, we'll conduct some refactoring and demonstrate how to use dapper together with the dependency injection. There will be no visible functionalities added - we will just focus on making our code more organized and versatile.  

The services 

I’m fairly sure that some of you were wondering about the lack of services in the demo project. The time has come to finally introduce them to our application and continue to use them in the next posts. At this point, we will be using only one service, and that service will be responsible for setting up and getting the data. At this point I have a little conundrum - should I also add Interface or not? When it comes to the interfaces there are two groups of people with two approaches. First of them goes with the approach that service class, well any class that’s get injected to be specific, should always have an interface implemented. The second group of people, on the other hand, believes the interfaces should be introduced only if they are absolutely necessary. I’m not going to discuss getting into it, let’s just say that we're going to use an interface. 

Before we start coding, I'd like to show you the structure of my project so that we're all on the same page. 

 

The interface is going to be placed in the Interfaces folder. The name of the interface is ITestService and it is placed in the ITestService.cs file. It contains the following code:   

1 2 3 4 5 6 7 namespace StoreAPI.Interfaces; public interface ITestService { Task SetValue(string storeName, string key, int testValue); Task<int> GetValue(string storeName, string key); }

The presented interface is rather straightforward. It has two asynchronous methods - one for setting up, and one for getting the data. The methods use the same set of arguments that we used to use to communicate with Dapr storage.  

We need to create service implementation now - well, implementations to be more specific. Why multiple, you may ask? It is because the second one will be just a mock-up to show good practices in dependency injection configuration when working will solutions such as Dapr. 

As you can see in the project structure the services got planned in the Services folder. One is intended to use dapr, while the other is just a mock-up at this point. 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 using StoreAPI.Interfaces; using Dapr.Client; namespace StoreAPI.Services; public class TestServiceDapr: ITestService { private readonly ILogger<TestServiceDapr> _logger; private readonly DaprClient _daprClient; public TestServiceDapr(ILogger<TestServiceDapr> logger, DaprClient daprClient) { _logger = logger; _daprClient = daprClient; } public async Task SetValue(string storeName, string key, int testValue) { await _daprClient.SaveStateAsync(storeName, key, testValue); } public async Task<int> GetValue(string storeName, string key) { return await _daprClient.GetStateAsync<int>(storeName, key); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 using StoreAPI.Interfaces; namespace StoreAPI.Services; public class TestServiceOther: ITestService { public Task SetValue(string storeName, string key, int testValue) { throw new NotImplementedException(); } public Task<int> GetValue(string storeName, string key) { throw new NotImplementedException(); } }

They are both quite self-explanatory - the first acts as a dapr communication layer and provides async methods, whilst the second provides methods with the same signature but both throw a Not Implemented Exception. 

The service is here to provide some possible alternatives for using Dapr infrastructure. Remember that Dapr is simply a layer - in this example, a storage layer - and that we could easily replace it with any other storage and implement that logic into another service. That is the appeal of layers. 

Dependency Injection 

Now that we have two services that implement the interface, it is time to rewrite the dependency injection code. But first, I'd like to remind you of how we're going to deal with the project – for that, we will be using PowerShell.  

1 2 3 4 5 6 dapr run ` --app-id storeAPI ` --app-port 7068 ` --dapr-http-port 3500 ` --components-path ../dapr/components ` dotnet run

The values passed in the call are going to make an impact in a second, but before we get into that I want to bring your attention to a section of the Program.cs file: 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); var daprPort = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); if (string.IsNullOrEmpty(daprPort)) { builder.Services.AddScoped<ITestService, TestServiceOther>(); } else { builder.Services.AddDaprClient(); builder.Services.AddScoped<ITestService, TestServiceDapr>(); } // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();

The part that we’re interested in is in the “Add services to the container” section. We are considering both the scenario in which we employ Dapr and the one in which we do not. And we do it by verifying the environmental value for DAPR HTTP PORT – and yes, this is the value that is set when we use the PowerShell script to start our application using Dapr. As a result, we can simply verify whether we are using Dapr and alter our injection setup. 

If the value is empty, all we need to do is inject the service that is not using Dapr. 

1 2 3 4 if (string.IsNullOrEmpty(daprPort)) { builder.Services.AddScoped<ITestService, TestServiceOther>(); }

If the value is not empty it means that we are using Dapr, so the dependency injection configuration has to reflect that. I will make it happen by adding Dapr client and matching test Service. 

1 2 3 4 5 else { builder.Services.AddDaprClient(); builder.Services.AddScoped<ITestService, TestServiceDapr>(); }

The controller 

Now that the services injection is configured, we need to refactor the controller. There shouldn’t be any service logic in the controller. The refactored controller looks like this: 

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 using Microsoft.AspNetCore.Mvc; using StoreAPI.Interfaces; namespace StoreAPI.Controllers; [ApiController] [Route("[controller]")] public class DaprTestController: ControllerBase { private readonly ITestService _testService; private const string stateStoreName = "storestate"; public DaprTestController(ITestService testService) { _testService = testService; } [HttpGet("SetValue/{testValue}")] public async Task<ActionResult> SetValue(int testValue) { await _testService.SetValue(stateStoreName, "testValue", testValue); return Ok(); } [HttpGet("GetValue")] public async Task<ActionResult<int>> GetValue() { var result = await _testService.GetValue(stateStoreName, "testValue"); return result; } }

Now the controller constructor takes only one argument, the service instance. Depending on the configuration and running conditions, the arguments are injected. Controller endpoints simply make use of injected service, unaware of the storage technology employed. That is the separation of concern in a nutshell. 

Running and testing 

Now that all is in place application can be started, mine is Docker. It must be running for dapr to work. Once the application is running, we need to go to https://localhost:7068/swagger/index.html and try to set a value using the Swagger UI.  

As you can see all is working on that value was set to 21. Now to read the value... 

And the value is there. It was successfully read and, as predicted, it has a value of 21. So surprisingly enough, the refactoring process did break the code. Good job we.  

 

Summary  

 

We got a lot done today and you did very well if you kept up with all of that. We managed to move the actual logic to the services and inject them into the controller. Plus we are determining dependencies based on changing factors. That is a very solid piece of coding.  

If you have any questions, please drop me a line at karol.rogowski@softwarehut.com. 

Till next time. Keep coding. 



Author
Karol Rogowski
Head Of Engineering

Working in IT since 2009. Currently working as Head of Engineering at SoftwareHut and as an academic teacher at Białystok Technical University. Co-founder of meet.js Białystok. Book and articles author. Father, husband, huge H.P. Lovecraft fan and terrible poker player.