C
C#10mo ago
salty_pepper

Who handles dependency Injection in a class library?

Suppose there is a class library TestService which requires some dependencies. Whose responsibility would it be to handle it? 1. Let caller handle DI. This means caller will need to know about the underlying classes in the class library to be able to come up with the dependencies and use it. Seems a bit cumbersome? Like new TestService(dep1, dep2...), where deps could be interfaces defined in the class library 2. Providing default implementation in the class library ( maybe in constructor? ), so if dependencies are not passed in when creating the service, we create default concrete classes within the class library constructor and use them instead. // make parameters optional, and create actual deps inside the constructor TestService(null, null) 3. Other ways?
31 Replies
casog
casog10mo ago
Maybe define functionality for DL injection methods as interfaces in the library and make the user handle the ancillary DI config through this mechanism first which enables the use of the rest of library. Or a middleware handler that will allow you to inject all necessary dependencies at startup based on the libraries middleware handler extension , like the aspnet.security.oauth2.providers library. I’m interested in this as well because today I ran into compiling the eBay sdk for notification api and it was just interfaces and concrete solutions for which I had to extract all concretes and implement in my consuming project and inject with the libraries interface
salty_pepper
salty_pepperOP10mo ago
Yes. Currently this is how it works. TestService takes in some deps which are all interfaces. In the class library I've also implemented concrete classes based on those interfaces. I'm expecting the caller to create instances of the concrete classes first, and then pass those instances when creating TestService. Does this sound like a good approach?
casog
casog10mo ago
If it’s good enough for eBay….
salty_pepper
salty_pepperOP10mo ago
Lol
casog
casog10mo ago
I’d personally create a middleware extension where you can go builder.services.AddMYCoolLibrary(provider => { options.Dep1; … options.Dep2… }) Centralized and clean That way if they want to use your library they gotta know what dependencies are available and required Or you can use autofac
salty_pepper
salty_pepperOP10mo ago
I think middleware extension sounds like a good idea, don't wanna complicate it too much
Odd_Dev_404
Odd_Dev_40410mo ago
Maybe you can use .NET IoC container
Pobiega
Pobiega10mo ago
Extension method is how most libraries do it. Simply add a dependency on Microsoft.Extensions.DependencyInjection.Abstractions
salty_pepper
salty_pepperOP10mo ago
@Pobiega Interesting. I looked up online, looks like way of doing it is extending IServiceCollection in the library? Something like below, and then call this method inside configure service in startup.cs?
public static class ServiceCollectionExtensions
{
public static void AddTest(this IServiceCollection services)
{
services.AddScoped<ITestManager, TestManager>();
services.AddScoped<ITestService, TestService>();
}
}
public static class ServiceCollectionExtensions
{
public static void AddTest(this IServiceCollection services)
{
services.AddScoped<ITestManager, TestManager>();
services.AddScoped<ITestService, TestService>();
}
}
Microsoft.Extensions.DependencyInjection.Abstractions looks interesting, I'll look it up thanks
Pobiega
Pobiega10mo ago
yes the Microsoft.Extensions.DependencyInjection.Abstractions exposes IServiceCollection thats why you need it
salty_pepper
salty_pepperOP10mo ago
Ah yep I just saw that too, thanks! So in the main project, the user would instantiate those services with parameterisied constructors manually, then calling this static method should resolve all the other ones as it resolves all the dependencies recursively.
Pobiega
Pobiega10mo ago
no, most of the time the user would only call the extensionmethod builder.Services.AddSaltPeppersLibrary(); and thats it
salty_pepper
salty_pepperOP10mo ago
Somethings would require instantiating though. Eg. A concrete repository class constructor would take in a connection string, wouldn't the user need to instantiate it manually though before doing the mapping?
Pobiega
Pobiega10mo ago
no stuff like a connection string would be injected into the repository, probably via a config object look at how EntityFramework does it, for example
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DBConnection")));

services.AddControllers();
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DBConnection")));

services.AddControllers();
so you can add your own extensionmethod overload that takes a configuration action, like AddDbContext does, or you simply configure the options object in your extention method and inject it into your repository
salty_pepper
salty_pepperOP10mo ago
Yep. So normally repository call something like ConfigurationManager.ConnectionStrings["applicationDatabase"].ConnectionString; to get connection string. So I just map IConfigurationManagerWrapper to ConfigurationManagerWrapper like below and it should auto resolve then right?
public class ConfigurationManagerWrapper : IConfigurationManagerWrapper {
public string GetConnectionString() {
return ConfigurationManager.ConnectionStrings["applicationDatabase"].ConnectionString;
}
}
public class ConfigurationManagerWrapper : IConfigurationManagerWrapper {
public string GetConnectionString() {
return ConfigurationManager.ConnectionStrings["applicationDatabase"].ConnectionString;
}
}
Pobiega
Pobiega10mo ago
what, no ConfigurationManager is dead thats .net framework stuff
salty_pepper
salty_pepperOP10mo ago
Repo takes in IConfigurationManagerWrapper
public PositionRepository(IConfigurationManagerWrapper config) {
_connectionString = config.GetConnectionString();
}
public PositionRepository(IConfigurationManagerWrapper config) {
_connectionString = config.GetConnectionString();
}
Pobiega
Pobiega10mo ago
Something along those lines would probably work, yeah, but the exact implementation bothers me as in, the idea is correct
salty_pepper
salty_pepperOP10mo ago
lmao Rip ConfigurationManager :we_have_no_bugs: Right, so it works but it's ancient
Pobiega
Pobiega10mo ago
yeah, its the "old" way of doing application config
salty_pepper
salty_pepperOP10mo ago
Would newer way be using IConfiguration ?
Pobiega
Pobiega10mo ago
the new one, IConfiguration + IOptions<T> warning: big post incoming $options
MODiX
MODiX10mo ago
appsettings.json:
{
"My": {
"Foo": {
"Kix": 5
}
}
}
{
"My": {
"Foo": {
"Kix": 5
}
}
}
src/Foo/FooOptions.cs:
public class FooOptions
{
public const string SectionName = "My:Foo";

public string Bar {get;set;} = "default value for bar";
public int Kix {get;set;} = -1;
public DateTime? Pouet {get;set;} = default;
}
public class FooOptions
{
public const string SectionName = "My:Foo";

public string Bar {get;set;} = "default value for bar";
public int Kix {get;set;} = -1;
public DateTime? Pouet {get;set;} = default;
}
src/Foo/FooServiceCollectionExtensions.cs:
namespace Microsoft.Extensions.DependencyInjection; // <==== recommanded for service.Add so that you don't clutter Startup file

public class FooServiceCollectionExtensions
{
public static IServiceCollection AddFoo(this IServiceCollection services) =>
services
.AddOptions<FooOptions>()
.BindConfiguration(FooOptions.SectionName)
.Validate(options => options.Kix >= 0, $"The configuration key '{FooOptions.SectionName}:{nameof(Kix)}' cannot be negative")
;

public static IServiceCollection AddFoo(this IServiceCollection services, Action<FooOptions> configure) =>
services
.AddFoo()
.Configure(configure);
namespace Microsoft.Extensions.DependencyInjection; // <==== recommanded for service.Add so that you don't clutter Startup file

public class FooServiceCollectionExtensions
{
public static IServiceCollection AddFoo(this IServiceCollection services) =>
services
.AddOptions<FooOptions>()
.BindConfiguration(FooOptions.SectionName)
.Validate(options => options.Kix >= 0, $"The configuration key '{FooOptions.SectionName}:{nameof(Kix)}' cannot be negative")
;

public static IServiceCollection AddFoo(this IServiceCollection services, Action<FooOptions> configure) =>
services
.AddFoo()
.Configure(configure);
Program.cs / Startup.cs:
services.AddFoo();
// or
services.AddFoo(fooOptions => fooOptions.Kix = 12);
services.AddFoo();
// or
services.AddFoo(fooOptions => fooOptions.Kix = 12);
Bar.cs:
public class Bar
{
private readonly FooOptions _fooOptions;

// .Value in ctor is fine only if it's always ever a non-changing value (no reload and/or no scoped resolution)
public Bar(IOptions<FooOptions> fooOptions)
=> _fooOptions = fooOptions.Value;
}
public class Bar
{
private readonly FooOptions _fooOptions;

// .Value in ctor is fine only if it's always ever a non-changing value (no reload and/or no scoped resolution)
public Bar(IOptions<FooOptions> fooOptions)
=> _fooOptions = fooOptions.Value;
}
Pobiega
Pobiega10mo ago
Bar here is your repository
salty_pepper
salty_pepperOP10mo ago
Right, so this appsettings.json is stored in the caller project. But class libraries should be agnostic of such settings, which is why you defined FooOptions.cs right? Thanks btw!
Pobiega
Pobiega10mo ago
appsettings is from the "main" project, yes class libs generally dont add their own configuration files, the beauty of IConfiguration is that it doesnt specify how a config is stored you can add settings from a json file, a yaml file, environment variables, the CLI arguments, a database, whatever if you want support for something that doesnt already exist, its easy to make your own configuration provider
salty_pepper
salty_pepperOP10mo ago
Right, the actual project using this library will be configuring the config via ConfigurationBuilder, and we will add the settings files from there. Then we do something like services.Configure<XXXSettings>(Configuration.GetSection("XXXSettings")); And in the class library we can inject IOptions<XXXSettings> Am I in 2024 now? 😆
Pobiega
Pobiega10mo ago
yes services.Configure<XXXSettings>(Configuration.GetSection("XXXSettings")); would be in your libs extension method, as would the FooOptions class itself so the only thing the user does is add the config lines to their config file/provider of choice and call your ext method
salty_pepper
salty_pepperOP10mo ago
Much is appreciated! 🙏 Think I got this! The code you wrote helped out a lot! Extension method
public static class ServiceCollectionExtensions {
// Call this method in the main project
public static void AddCandidateService(this IServiceCollection services) {
services
.AddOptions<DatabaseOptions>()
.BindConfiguration(DatabaseOptions.SectionName);
services.AddScoped<IPositionRepository, PositionRepository>();
services.AddScoped<ICandidateRepository, CandidateRepository>();
services.AddScoped<ICandidateFactory, CandidateFactory>();
services.AddScoped<ICandidateCreditService, CandidateCreditServiceClient>();
services.AddTransient<ITimeProvider, TimeProvider>();
}
}
public static class ServiceCollectionExtensions {
// Call this method in the main project
public static void AddCandidateService(this IServiceCollection services) {
services
.AddOptions<DatabaseOptions>()
.BindConfiguration(DatabaseOptions.SectionName);
services.AddScoped<IPositionRepository, PositionRepository>();
services.AddScoped<ICandidateRepository, CandidateRepository>();
services.AddScoped<ICandidateFactory, CandidateFactory>();
services.AddScoped<ICandidateCreditService, CandidateCreditServiceClient>();
services.AddTransient<ITimeProvider, TimeProvider>();
}
}
This is the DatabaseOptions class
namespace Refactoring.LegacyService;

public class DatabaseOptions {

public const string SectionName = "ConnectionStrings";

public string ApplicationDatabase { get; set; } = string.Empty;
}
namespace Refactoring.LegacyService;

public class DatabaseOptions {

public const string SectionName = "ConnectionStrings";

public string ApplicationDatabase { get; set; } = string.Empty;
}
It would be injected into a class like below, eg. in a repository
public CandidateRepository(IOptions<DatabaseOptions> options) {
_connectionString = options.Value.ApplicationDatabase;
}
public CandidateRepository(IOptions<DatabaseOptions> options) {
_connectionString = options.Value.ApplicationDatabase;
}
Pobiega
Pobiega10mo ago
yup
salty_pepper
salty_pepperOP10mo ago
tack sa mycket 🙏 🙏 🙏
Want results from more Discord servers?
Add your server