Dependency Injection Notes - Habilya/LearningCourseNotes GitHub Wiki

Use Extension methods in IOC container for fluidity

var builder = WebApplication.CreateBuilder(args);

// ConfigureServices starts
builder.Services.AddEndpointsApiExplorer();

// ConfigureServices ends
var app = builder.Build();


// this groups the logical services together to not pollute the configure services section
public static class SomethingServiceCollectionExtensions
{
	public static IServiceCollection AddEndpointsApiExplorer(
		this IServiceCollection services)
	{
		services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();
		services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
		services.TryAddEnumerable(
			ServiceDescriptor.Transient<IApiDescriptionProvider, EndpointMetadataApiDescriptionProvider>());
		return services;
	}
}

LifeTimes of dependencies

  • Singleton - Single instance of a dependency will be created and always returned throughout the lifetime of whole application
  • Transient - Every time you require it, new instance of dependency will be returned
  • Scoped - Dependency lifetime is the scope of instance of a class

GetService GetRequiredService

// or IServiceProvider serviceProvider
var serviceProvider = services.BuildServiceProvider();

// throws an exception if unresolved service
var someservice = serviceProvider.GetRequiredService<OldFilesRemover>();
// returns null if unresolved
var someservice2 = serviceProvider.GetService<OldFilesRemover>();

Resolving dependencies in service registration

buildrt.Services.AddScoped(provider => 
{
    var logger = provider.GetRequiredService<ILogger<DurationLoggerFilter>>();
    return new DurationLoggerFilter(logger);
});

Clean up service registration

services registration can be grouped into logical methods create a file DependencyInjection.cs

// !!!!!!! Note the namespace usage here -- the consuming class, now doesn't have to add using statement for this class
// Green squigly lines, but it's Ok
namespace Microsoft.Extensions.DependencyInjection;

public static class DependencyInjection
{
	public static ILoggingBuilder AddTeamUpWebScraperLogging(this ILoggingBuilder loggingBuilder, IConfiguration configuration)
	{
		var serilogLogger = new LoggerConfiguration()
			.ReadFrom.Configuration(configuration)
			.Enrich.FromLogContext()
			.CreateLogger();

		// Clear default logging providers and add Serilog
		loggingBuilder.ClearProviders();
		loggingBuilder.AddSerilog(serilogLogger);

		return loggingBuilder;
	}

	public static IServiceCollection AddTeamUpWebScraperLibraryServices(this IServiceCollection services, HostBuilderContext context)
	{
		// Register the services from the library
		services.AddSingleton<IDateTimeProvider, DateTimeProvider>();
		services.AddTransient(typeof(ILoggerAdapter<>), typeof(LoggerAdapter<>));
		services.AddSingleton<InputValidation>();
		services.AddSingleton<ITeamUpAPIService, TeamUpAPIService>();
		services.AddSingleton<IXLWorkBookFactory, XLWorkBookFactory>();
		services.AddSingleton<IEventApiResponseTransformer, EventApiResponseTransformer>();
		services.AddSingleton<IExcelSpreadsheetReportProvider, ExcelSpreadsheetReportProvider>();
		services.AddSingleton<IDisplayGridViewProvider, DisplayGridViewProvider>();

		// Example of adding HttpClient if needed for the library
		services.AddHttpClient(TeamUpApiConstants.HTTP_CLIENTNAME, (serviceProvider, httpClient) =>
		{
			var config = serviceProvider.GetRequiredService<IConfiguration>();
			var baseURL = config.GetValue<string>($"{AppsettingsConstants.CONFIG_SECTION_NAME_TEAMUP_API}:{TeamUpApiConstants.CONFIG_BaseURL_NAME}");
			var calendarId = config.GetValue<string>($"{AppsettingsConstants.CONFIG_SECTION_NAME_TEAMUP_API}:{TeamUpApiConstants.CONFIG_CalendarId_NAME}");
			var teamupToken = config.GetValue<string>($"{AppsettingsConstants.CONFIG_SECTION_NAME_TEAMUP_API}:{TeamUpApiConstants.CONFIG_TeamupToken_NAME}");

			httpClient.BaseAddress = new Uri(baseURL + calendarId + "/");
			httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
			httpClient.DefaultRequestHeaders.Add(TeamUpApiConstants.API_TOKEN_HEADER_NAME, teamupToken);
		});

		// Read appsettings.json into appropriate models

		// !!! BUG or FEATURE? !!! This Bind is uncapable of reading model annotations
		// ex: [JsonPropertyName("name")]
		// Probably because config is supposed to be concieved by the developer
		// and everythig is supposed to be rightly named
		#region TeamUpApiConfiguration as a dependency
		var teamUpApiConfiguration = new TeamUpApiConfiguration();
		context.Configuration.GetSection(AppsettingsConstants.CONFIG_SECTION_NAME_TEAMUP_API).Bind(teamUpApiConfiguration);
		services.AddSingleton(teamUpApiConfiguration);
		#endregion

		#region ExcelReportSpreadSheetConfiguration as a dependency
		var excelReportSpreadSheetConfiguration = new ExcelReportSpreadSheetConfig();
		context.Configuration.GetSection(AppsettingsConstants.CONFIG_SECTION_NAME_EXCEL_SPREADSHEET).Bind(excelReportSpreadSheetConfiguration);
		services.AddSingleton(excelReportSpreadSheetConfiguration);
		#endregion

		// Add other necessary services as needed
		return services;
	}
}

in program.cs

static IHostBuilder CreateHostBuilder()
{
	return Host
		.CreateDefaultBuilder()
		.ConfigureAppConfiguration((hostContext, config) =>
		{
			config.AddJsonFile(CONFIG_JSON_FILE_PATH, optional: false)
				.AddEnvironmentVariables()
				.Build();
		})
		.ConfigureLogging((hostContext, logging) =>
		{
			logging.AddTeamUpWebScraperLogging(hostContext.Configuration);
		})
		.ConfigureServices((context, services) =>
		{
			services.AddTransient<Dashboard>();
			services.AddSingleton<TeamUpController>();
			services.AddTeamUpWebScraperLibraryServices(context);
		});
}

Creating decorators

Timing an API request

public class WeatherService : IWeatherService 
{
	public async Task<WeatherResponse?> GetCurrentWeatherAsync(string city)
	{
		var sw = Stopwatch.StartNew();
		try 
		{
			var url = $"https://someapi.org/?q={city}";
			var httpClient = _httpClientFactory.CreateClient();
			
			var weatherResponse = await httpClient.GetAsync(url);
			if(weatherResponse.StatusCode == HttpStatusCode.NotFound) 
			{
				return null;
			}
			
			var weather = await weatherResponse.Content.ReadFromJsonAsync<WeatherResponse>();
			return weather;
		}
		finally 
		{
			sw.Stop();
			_logger.LogInformation($"Weather retrieval for city {0}, took {1}ms",
				city, sw.ElapsedMilliseconds);
		}
	}	
}

program.cs

services.AddTransient<IWeatherService, OpenWeatherService>();

metric collection should not be part of service API call...

public class LoggedWeatherService : IWeatherService // <-- Decorator of OpenWeatherService
{
	private readonly IWeatherService _weatherService; // <-- OpenWeatherService
	private readonly ILogger<IWeatherService> _logger;
	
	public LoggedWeatherService(IWeatherService weatherService,
		ILogger<IWeatherService> logger)
	{
		_weatherService = weatherService;
		_logger = logger;
	}
	
	public async Task<WeatherResponse?> GetCurrentWeatherAsync(string city)
	{
		var sw = Stopwatch.StartNew();
		try 
		{
			return await _weatherService.GetCurrentWeatherAsync(city);
		}
		finally 
		{
			sw.Stop();
			_logger.LogInformation($"Weather retrieval for city {0}, took {1}ms",
				city, sw.ElapsedMilliseconds);
		}
	}	
}

program.cs

services.AddTransient<OpenWeatherService>();
services.AddTransient<IWeatherService>(provider => 
    new LoggedWeatherService(provider.GetRequiredService<OpenWeatherService>(),
        provider.GetRequiredService<ILogger<IWeatherService>>()));
⚠️ **GitHub.com Fallback** ⚠️