I'm learning dependency injection, because I don't want my BE to look spaghety no more. I have a good understanding of Asp.Net Core and EF Core. I just never learned dependecy injection properly. I'm playing around with an idea. Let's say, that I create an EmailSenderService
(and IEmailSenderService
with it). I do the same for CustomLogger
and WeatherRepository
. Here are the implementations:
Program.cs:
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddScoped<ICustomLogger, CustomLogger>();
builder.Services.AddScoped<IEmailSenderService, EmailSenderService>();
builder.Services.AddScoped<IWeatherRepository, WeatherRepository>();
// Add swagger
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();
}
CustomLogger.cs
public interface ICustomLogger
{
public void Log(string logText);
}
public class CustomLogger : ICustomLogger
{
public void Log(string logText) => System.Diagnostics.Debug.WriteLine(logText);
}
EmailSenderService.cs
public interface IEmailSenderService
{
public void SendMail(string email, string text);
}
public class EmailSenderService : IEmailSenderService
{
public void SendMail(string email, string text) => System.Diagnostics.Debug.WriteLine($"TO: {email}, TEXT: {text}");
}
WeatherForecastModel.cs
public struct WeatherForecastModel
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
WeatherRepository.cs
public interface IWeatherRepository
{
public WeatherForecastModel[] GetRandomSample();
}
public class WeatherRepository : IWeatherRepository
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public WeatherForecastModel[] GetRandomSample() =>
Enumerable.Range(1, 5).Select(index => new WeatherForecastModel
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
}).ToArray();
}
WeatherForecastController.cs
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ICustomLogger _customLogger;
private readonly IEmailSenderService _emailSenderService;
private readonly IWeatherRepository _weatherRepository;
public WeatherForecastController(ICustomLogger customLogger, IEmailSenderService emailSenderService, IWeatherRepository weatherRepository)
{
_customLogger = customLogger;
_emailSenderService = emailSenderService;
_weatherRepository = weatherRepository;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecastModel> Get()
{
_customLogger.Log("Started function GetWeatherForecast");
WeatherForecastModel[] results = _weatherRepository.GetRandomSample();
_customLogger.Log("Started sending mail.");
_emailSenderService.SendMail("some.mail@domain.com", $"Summary of the first result: {results[0].Summary}");
_customLogger.Log("Ended sending mail.");
_customLogger.Log("Ended function GetWeatherForecast");
return results;
}
}
Now, with the whole implementation, in place, I don't like it. Like visually. I do not want to see logging and email sending logic inside of my controller. This is the fundamentaly issue, I'm trying to solve with this question. I could (I implemented it for testing) inject logger inside the EmailSenderService
and inside the WeatherRepository
and log there, howerver, I do not like that either. I do not want to see logging inside of my logic. So, I thought about something I called LogAwareEmailSenderService
. Here is impelementation:
public class LogAwareEmailSenderService : IEmailSenderService
{
private readonly ICustomLogger _customLogger;
private readonly IEmailSenderService _emailSenderService;
public LogAwareEmailSenderService(ICustomLogger customLogger, IEmailSenderService emailSenderService)
{
_customLogger = customLogger;
_emailSenderService = emailSenderService;
}
public void SendMail(string email, string text)
{
_customLogger.Log($"Started sending email to: {email}, containing text: {text}");
_emailSenderService.SendMail(email, text);
_customLogger.Log($"Done sending email to: {email}, containing text: {text}");
}
}
Basically, what I'm trying to achieve, is: Take my original EmailSenderService
, then inject it into my LogAwareEmailSenderService
. The idea is, that now, I should be able to inject this LogAwareEmailSenderService
into my controller without the need to change my controller at all (just remove my previous logging logic), right? And If I achieve this, I can go on and continue, to make something like LogAndEmailAwareWeatherRepository
, that will inject LogAwareEmailSenderService
and instead of sending mail and logging function start inside of the controller. I will just call LogAndEmailAwareWeatherRepository
, that will log these things, and send the email, resulting in controller only calling the important, _weatherRepository.GetRandomSample()
-- This call will do the logging and sending mail, using the previously described abstractions.
However, in the first place, I am unable to inject the EmailSenderService
inside the LogAwareEmailSenderService
. I want them both to be scoped. I trid this approach (in my Program.cs):
builder.Services.AddScoped<IEmailSenderService, EmailSenderService>();
builder.Services.AddScoped<IEmailSenderService, LogAwareEmailSenderService>();
however I got circular dependency error:
'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: DependencyInjectionExample.Services.EmailSenderService.IEmailSenderService Lifetime: Scoped ImplementationType: DependencyInjectionExample.Services.EmailSenderService.LogAwareEmailSenderService': A circular dependency was detected for the service of type 'DependencyInjectionExample.Services.EmailSenderService.IEmailSenderService'.
DependencyInjectionExample.Services.EmailSenderService.IEmailSenderService(DependencyInjectionExample.Services.EmailSenderService.LogAwareEmailSenderService) -> DependencyInjectionExample.Services.EmailSenderService.IEmailSenderService)'
So, I got some questions:
- Am I going about this right? Like, is what I described above, the normal approach to things?
- Where should I put my Logging logic? When doing this, I also thought about caching things, meaning, that I would have something like
CacheAwareWeatherRepository
, that would only care about the caching implementation and then call the WeatherRepository to get data and return them, while caching them. - How to implement my solution?
- I still don't understand some parts of dependecy injection, are there any articles/books that helped you personally understand it?
If you've got here, thank you, I know it is long, however I wanted to describe my problem, possible solutions, and questions clearly.
If you have any questions, about anything, please feel free to ask me in comments, or email me (if it's long question) to dibla.tomas@email.cz. I would really like to get to the bottom of this.
PS: This is not about implementation of bussiness logic, or anything like this, this is only for getting data, logging it, caching it and doing abstractions above data access. I implemented this, with idea that you would have one interface and then layers of abstractions. One for getting the actual data (fAb
), One for logging the fact (sAb
) implementing fAb
, One for caching data (tAb
) implementing sAb
, One for logging the fact of caching (qAb
) implementing tAb
. And so on.