Dependency Injection & Common App Settings
Hi guys, how you doing? Today I want to talk about common app settings, what do I want to mean? Sometimes, our app has immutable settings, in other words, that settings will not change in the whole app life. For example:
- App Name
- App email
- Total rows size of app’s grids
- Attachment folder path
- Some plugin keys, etc
We have many ways to save and to retrieve this information, we will have a look some alternatives 😉
Business scenario 😀
We are developing an app for a Travel Agency. In this app:
- The grids are refreshed every 2 minutes automatically if the user is on this page.
- All grids support pagination, the page size for all grids is 25 rows per page.
- The user can upload files to a specific storage address in the server
Ok, we don’t like to hardcode values in our code, for this reason, we will use 3 parameters:
- RefreshGridTimerInSeconds
- RowsPerPage
- StorageAddress
The app must consult these parameters all the time to load grids or upload files.
Alternative 1 – Use appSettings & Options Pattern 🙂
Firstly, we will declare an “AppSettings” section in the appSettings.json:
{
...
"AppSettings": {
"RefreshGridTimerInSeconds": 120,
"RowsPerPage": 25,
"StorageAddress": "E:\\Storage"
}
}
Then, we need to create a class to map these properties:
public class AppSettings
{
public int RefreshGridTimerInSeconds { get; set; }
public int RowsPerPage { get; set; }
public string StorageAddress { get; set; }
}
Finally, we have to add the Dependency Injection configuration, in other words, add our class to the container.
We use the Options Pattern to inject our AppSettings in the classes that need it. For doing that, we have to register the configuration in the container:
//Startup class
public void ConfigureServices(IServiceCollection services)
{
//Configure DI - Add AppSettings in the container
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
...
And now ? What do we have to do ? Easy, we need to inject IOption<T> or IOptionsSnapshot<T> or maybe IOptionsMonitor<T> in the class that wants to use AppSettings class.
In this case, we use IOptionsSnapshot<T> because we want to retrieve the last value of the AppSettings and this class check the value in each request, it’s a SCOPED Service (I will do a post talking about these interesting classes later 😉 ) .
Let’s inject the appSettings in our Destination controller:
[Route("api/[controller]")]
[ApiController]
public class DestinationsController : ControllerBase
{
private readonly AppSettings _appSettings;
public DestinationsController(IOptionsSnapshot<AppSettings> appSettings)
{
_appSettings = appSettings.Value;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
_appSettings.RefreshGridTimerInSeconds,
_appSettings.RowsPerPage,
_appSettings.StorageAddress
});
}
}
When we send a request to api/destinations [GET] :
Excellent, using this way, we can retrieve the settings from the appSettings.json wherever we want 😉 only need to use dependency injection 😉
Alternative 2 – Use appSettings & Singleton 😀
Another alternative to take could be to create a Singleton Service to retrieve the settings.
In this scenario, the settings are immutable for this reason when we register the singleton pattern, we need to use the Factory overload to create an instance and pass it the initial parameters.
Firstly, we create our AppSetting service and its interface:
public class AppSettingService: IAppSettingService
{
public int RefreshGridTimerInSeconds { get; private set; }
public int RowsPerPage { get; private set; }
public string StorageAddress { get; private set; }
public AppSettingService(int refreshGridTimerInSeconds, int rowsPerPage, string storageAddress)
{
RefreshGridTimerInSeconds = refreshGridTimerInSeconds;
RowsPerPage = rowsPerPage;
StorageAddress = storageAddress;
}
}
Then, we have to register this service as Singleton. In this case, we use the Factory overloading to pass the initial values to the class.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IAppSettingService, AppSettingService>(sp =>
{
return new AppSettingService(
Convert.ToInt32(Configuration["AppSettings:RefreshGridTimerInSeconds"]),
Convert.ToInt32(Configuration["AppSettings:RowsPerPage"]),
Configuration["AppSettings:StorageAddress"]);
});
...
Finally, we inject the service in our Destination controller:
[Route("api/[controller]")]
[ApiController]
public class DestinationsController : ControllerBase
{
private IAppSettingService _appSettingService;
public DestinationsController(IAppSettingService appSettingService)
{
_appSettingService = appSettingService;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new
{
_appSettingService.RefreshGridTimerInSeconds,
_appSettingService.RowsPerPage,
_appSettingService.StorageAddress
});
}
...
Have a look when we send a request to api/destinations:
That’s perfect 😉 We get the same result of the alternative 1;)
I prefer alternative 1 in immutable scenarios because we can take advantage of the Options pattern but when we need to apply some logic, such as to use the parameters to calculate other variables or so on, I take the alternative 2: Singleton pattern.
Alternative 3 – Mutable app settings, singleton service & scoped repository & IServiceScopeFactory 😮
Ok, all good in the hood until the customer call and told me that we need a module to update de app settings values! What? yeahh, he told me that the users need to update the parameters: RefreshGridTimerInSeconds, RowsPerPage, and StorageAddress one time per month or maybe more.
Ok, that could be a problem because I was taking the settings from the AppSettings.json and that is no longer an option. Now users have the option to update the settings 🙁
Ok, we will create an AppSettings Table in the BD with the columns: Id | Code | Value. In addition, we will use the Repository pattern to access the database.
Have a look at the AppSettings class. We will use it to map the table registers.
public class AppSettings
{
public int Id { get; set; }
public string Code { get; set; }
public string Value { get; set; }
}
Then, we create the IAppSettingRepository interface and AppSettingRepository concrete class:
public interface IAppSettingRepository
{
IEnumerable<AppSettings> GetList();
}
Finally, we need to register the repository as Scoped.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAppSettingRepository, AppSettingRepository>();
...
The AppSettingRepository concrete class is not important. We have two options right now:
- Inject IAppSettingRepository in the classes that need to read the settings: in other words, when a class needs to read the app settings values, the AppSettingRepository retrieve them from the database.
- Create a Singleton Service that has the setting values. Of course, this service has to use the AppSettingRepository to access to the database for initializing the singleton values and to refresh them when the user modifies them.
Ok, what is the best approach? In my opinion, alternative 2, why?
- In the first request that needs to access the settings: the singleton will load their variables accessing to the database using AppSetttingRepository.
- All subsequent requests that need to access app setting values will be able to use the Singleton Service and it doesn’t access the database because it has all settings loaded.
- When users update the Settings, the app updates the database and the singleton service must refresh their values too.
I think that it is a good approach however we have a last problem to resolve: we have a Singleton service and we can not inject the AppSetttingRepository in because it has a Scoped lifetime 🙁
Ok, we have a AppSettingService [Singleton] and it needs to use AppSettingRepository [Scoped], and of course, we can not inject a scoped service in a singleton 🙁 What alternative do we have ?
Ok, we can inject AppSettingRepository directly however we can inject IServiceScopeFactory ! this interface allows us to create a scope and using the provider to retrieve instances from the container manually 😉 In this case, we can get our AppSettingRepository 😉
public class AppSettingService: IAppSettingService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
//Settings
public int RefreshGridTimerInSeconds { get; private set; }
public int RowsPerPage { get; private set; }
public string StorageAddress { get; private set; }
//IServiceScopeFactory allows create scope and use the provider to retrieve instances from the container manually ;)
public AppSettingService(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
this.ReloadParameters();
}
//Using this method to init and refresh the settings
public void ReloadParameters()
{
using(var scope = _serviceScopeFactory.CreateScope())
{
var repository = scope.ServiceProvider.GetService<IAppSettingRepository>();
var settings = repository.GetList();
RefreshGridTimerInSeconds = Convert.ToInt32(settings.First(s => s.Code == "RefreshGridTimerInSeconds").Value);
RowsPerPage = Convert.ToInt32(settings.First(s => s.Code == "RowsPerPage").Value);
StorageAddress = settings.First(s => s.Code == "StorageAddress").Value;
}
}
}
Excellent 😉 we got it 😉
Have a look at the Destination controller, you can see two methods:
- Get to retrieve values from the singleton
- UpdateSettings to update values in the database and reload singleton settings.
[Route("api/[controller]")]
[ApiController]
public class DestinationsController : ControllerBase
{
private readonly IAppSettingService _appSettingService;
private readonly IAppSettingRepository _appSettingRepository;
public DestinationsController(IAppSettingService appSettingService,
IAppSettingRepository appSettingRepository)
{
_appSettingService = appSettingService;
_appSettingRepository = appSettingRepository;
}
[HttpGet]
public IActionResult Get()
{
//retrieve values from the singleton without accessing to databse
return Ok(new
{
_appSettingService.RefreshGridTimerInSeconds,
_appSettingService.RowsPerPage,
_appSettingService.StorageAddress
});
}
[HttpPut]
public IActionResult UpdateSettings()
{
//Update values in the database
foreach (var setting in _appSettingRepository.GetList())
{
if( setting.Code == "RefreshGridTimerInSeconds" ||
setting.Code == "RowsPerPage")
{
setting.Value = "500";
}
else
{
setting.Value = @"C:\Storage\Test";
}
}
//Refresh singleton settings
_appSettingService.ReloadParameters();
return NoContent();
}
}
Let’s test 😉
1- Init app, send a request to app/destinations, init singleton with database values, retrieve them and return:
Excellent, the singleton is initialized 😉
After that, we send a PUT request to update database values and refresh the singleton.
Finally, we retrieve the values again:
Great! the user can update the app settings and we can retrieve them from the Singleton 😉 magic 😀
That’s all guys, I hope you enjoyed it 😉
Code on my GitHub
Big hug 😀