How do I fix the dependency injection so that I can access .ForContext(...) within my worker/service class?
[.Net Core 6 for a hybrid console/WindowsService App]
In my main program class and worker/service classes, I have Serilog working correctly for basic logging...
Program.cs
Log.Information($"Application Directory: {baseDir}");
Worker.cs
_logger.LogInformation("Service Starting");
ServiceA.cs
_logger.LogInformation("In Service A");
However, the issue I'm having is that I need .ForContext to be able to work everywhere as well... and it does in my main program class:
Program.cs
Log
.ForContext("EventID", 42)
.Information("Log This with EventID === 42");
... however, when I try to do the same in either of the worker/service classes ...
Worker.cs
_logger
.ForContext("EventID", 42)
.Information("Log This with EventID === 42");
... it does not work, and I get the following error:
Error CS1061
'ILogger<Worker>' does not contain a definition for 'ForContext' and no accessible extension method 'ForContext' accepting a first argument of type 'ILogger<Worker>' could be found
... so I looked into that, and came upon the following SO questions (neither of which was I able to apply, see comments in code below) which were close:
Hot to get Serilog to work with Depedency Injection?
Serilog DI in ASP.NET Core, which ILogger interface to inject?
Inject Serilog's ILogger interface in ASP .NET Core Web API Controller
... (as well as some other places) but I was unable to integrate the answers into the codebase:
Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureAppConfiguration((context, config) =>
{
// Configure the app here.
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings"));
services.AddScoped<IServiceA, ServiceA>();
services.AddScoped<IServiceB, ServiceB>();
//?? I'm not sure if this .AddLogging(...) is needed ??
services.AddLogging(x =>
{
x.ClearProviders();
x.AddSerilog(dispose: true);
});
//?? ... having/not having it doesn't seem to affect execution
})
.UseSerilog();
Worker.cs
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
//private readonly Serilog.ILogger<Worker> _logger;
//?? ... wrt this attempt, Serilog.ILogger won't allow <Worker> ...
//?? private readonly ILogger _log = Log.ForContext<SomeService>();
//private readonly ILogger _logger = Log.ForContext<Worker>();
//?? ... wrt this attempt, Log isn't available ...
private FileSystemWatcher _folderWatcher;
private readonly string _inputFolder;
private readonly IServiceProvider _services;
public Worker(ILogger<Worker> logger, IOptions<AppSettings> settings, IServiceProvider services)
{
_logger = logger;
_services = services;
_inputFolder = settings.Value.InputFolder;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.CompletedTask;
}
public override Task StartAsync(CancellationToken cancellationToken)
{
_logger
.ForContext("EventID", 1001)
.LogInformation("Service Starting - need to assign EventID === 1001");
//?? here is where .ForContext is needed (but doesn't work)
... ... ...
Preface:
This answer uses "MEL" as an abbreviation for Microsoft.Extensions.Logging.
Because the type-name ILogger is used by both MEL and Serilog for completely different types, this answer always disambiguates occurrences of the name ILogger as either MEL.ILogger or Serilog.ILogger.
Though note that only MEL's ILogger<T> is generic. There is no Serilog.ILogger<T>, only Serilog.ILogger.
TL;DR (for .ForContext):
Serilog.ILogger.ForContext(String propertyName, Object? value, Boolean destructureObjects = false) creates a "child" Serilog.ILogger instance with a single event-property added to all events logged by the child logger.
Whereas in MEL, you would typically use ILogger<T>'s BeginScope in a using( log.BeginScope(...) ) block and passing state: Dictionary<String,Object?> parameter), which has different semantics to ForContext. This is why you should never mix Serilog idioms with MEL idioms.
Serilog.ILogger.ForContext(Type) and Serilog.ILogger.ForContext<TSource> are both equivalent to calling ForContext(String).
(ForContext<TSource>() passes typeof(TSource) into ForContext(Type), and ForContext(Type) just passes Type.FullName into ForContext(String)).
The MEL equivalent of ForContext<TSource> is to either....
...use constructor parameter dependency injection by injecting MEL's ILogger<TSource> as a TSource constructor parameter.
Inject (or otherwise obtain) a reference to MEL's ILoggerFactory and specify TSource as a generic type argument for the .CreateLogger<T> extension method.
Both of these approaches will make the MEL ILoggerFactory add a "SourceContext" (or "Category") event-property for typeof(TSource).FullName for you automatically during injection.
So if you are injecting MEL's ILogger<TService> into your class TService's constructor then...
...you don't need to call ForContext yourself to set "SourceContext" (or "Category").
And you should be using MEL's ILogger.BeginScope() instead of Serilog.ILogger.ForContext for when you want to add more event-properties inside your service.
If you try to use ForContext with MEL's ILogger (both the generic and non-generic versions) you'll get a compiler error, ditto if you try to use BeginScope with Serilog.ILogger - simply because those are extension methods that are only defined for their intended types.
Read on...
There are 2 different ways to use MEL and Serilog together in an application using Microsoft.Extensions.DependencyInjection.
Use Serilog only as a (host-specific) backend for MEL, while using MEL for the "frontend" MEL.ILogger<T> interfaces that are injected into your types' constructors.
Use Serilog directly in your own code (either with the static members of global::Serilog.Log, or by injecting global::Serilog.ILogger) while still wiring-up MEL for Serilog so that other components (that you didn't write) that use MEL will still appear in Serilog output.
Option 2 is the preference of Serilog's author...
In application code, I use Serilog.ILogger and static global::Serilog.Log.* methods exclusively.
I also dislike having to clutter every constructor with MEL.ILogger<T> parameters (constructor argument lists are highly-valuable real estate), and find there are usability issues once code steps out of DI and starts calling non-DI-instantiated classes that want to do some logging (more parameter passing).
...however I (respectfully) disagree with #nblumhardt's reasoning because:
Using any impure (i.e. non-side-effect-free) static APIs in general (not just Serilog's Serilog.Log) is a bad idea because managing static state in unit and integration-tests is very difficult, if not impossible, depending on the API you're using and how your test-runner works.
For example, if you want your unit-test or integration-test to make assertions about what was logged by your SUT then you cannot use concurrent test execution: they must be strictly sequential so that your test-environment can reset or reconfigure the Serilog.Log for each test case, otherwise all of the test output will be munged together.
The reasoning for avoiding "usability" and how "constructor argument lists are highly-valuable real-estate" are couched in ergonomics, or even mere aesthetics, however this is probably the worst reason for not doing something properly: "I'm going to cause engineering problems for myself because of aesthetics" is not a good case to present to your project's engineering manager.
While I appreciate that having ctor params makes it harder to reuse a class in situations where DI is unavailable, using the static Serilog.Log methods is no-better: it means it's now harder to reuse your class in situations where the static Serilog types are unavailable.
My preferred solution to that situation is to either define a static factory method for that type which supplies stock or NOOP implementations of MEL.ILogger<T> (e.g. NullLogger<T>) or define an alternative constructor that supplies its own defaults (and apply [ActivatorUtilitiesConstructor] to the DI constructor).
Also, Microsoft.Extensions.Logging is now established as the baseline logging library which is present in every .NET environment now - given any random .csproj project created in the past 5 years, it's far more likely that MEL.ILogger<T> will be available instead of (if not in addition to) the Serilog NuGet package.
It's only bare-bones console projects and older codebases, that aren't using IHost, that won't have MEL available. Also, every ASP.NET Core project has MEL anyway.
Things are different if you're using a different DI system, such as Simple Injector, Autofac, Ninject, and others, so please don't follow this post's advice if you're not using Microsoft.Extensions.DependencyInjection directly.
For option 1, this is how I do it in my projects:
If this is a multiple-project solution, with a single "entrypoint" .exe project that references your other projects...
Then the .exe project should reference the Microsoft.Extensions.Logging, Serilog, and Serilog.Extensions.Logging NuGet packages.
The other projects only need to reference Microsoft.Extensions.Logging.Abstractions (and not the main Microsoft.Extensions.Logging package).
If this is a single-project solution, then reference Microsoft.Extensions.Logging, Serilog, and Serilog.Extensions.Logging.
If you're using Host.CreateDefaultBuilder or WebHost.CreateDefaultBuilder then those methods already call .AddLogging for you already, you don't need to do it yourself, but you do need to call UseSerilog (Serilog.SerilogHostBuilderExtensions.UseSerilog) on the IHostBuilder before .Build() is called.
You also do not need to call .AddSerilog either.
Inside your service-types (i.e. your types that have service interfaces in their constructor params) use Microsoft.Extensions.Logging.ILogger<T> where T is the same type as the constructor's declaring type (yes, I agree this is redundant).
So you should not have using Serilog in any other .cs files besides your Program.cs file (and/or the file where your configureLogger method is).
Then, at runtime, when the Microsoft.Extensions.DependencyInjection container instantiates your types, it will call Microsoft.Extensions.Logging.ILoggerFactory.ILoggerFactory.CreateLogger(String categoryName) for you automatically.
(Where the categoryName is the type-name of T in the injected MEL.ILogger<T> type)
...which is passed-along to Serilog's ForContext logger factory, and the returned Serilog.ILogger is wrapped by MEL's MEL.ILogger<T>.
An actual example:
Program.cs
using System;
using Microsoft.Extensions.Logging;
// No `using Serilog` here.
static async Task<Int32> Main( String[] args )
{
using( IHost host = CreateHost( args ) )
{
await host.RunAsync();
return 0;
}
}
static IHost CreateHost( String[] args )
{
IHostBuilder b = Host.CreateDefaultBuilder( args );
// CreateDefaultBuilder calls `MEL.AddLogging` for you already.
b = b.UseStartup<YourStartupClass>(); // or `b.ConfigureServices()`;
// Explicit extension method call because there's no `using Serilog` to avoid ambiguity issues:
b = global::Serilog.SerilogHostBuilderExtensions.UseSerilog(
builder : b,
configureLogger : ConfigureSerilog,
preserveStaticLogger: true
);
return b.Build();
}
static void ConfigureSerilog( HostBuilderContext ctx, Serilog.LoggerConfiguration logCfg )
{
_ = logCfx
.Enrich.WithExceptionDetails()
.Enrich.FromLogContext()
.MinimumLevel.Is( /* etc */ )
.WriteTo.File( /* etc */ );
}
ExampleServiceType.cs
using System;
using Microsoft.Extensions.Logging;
// No `using Serilog` here either.
public MyService
{
private readonly ILogger log;
public MyService( ILogger<MyService> log )
{
this.log = log ?? throw new ArgumentNullException(nameof(log));
}
public void Foo( String name )
{
this.log.LogInformation( "hello {Name}", name );
}
}
If you see yourself reusing MyService in places where DI is unavailable, then you can define an alternative constructor, like so:
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.DependencyInjection;
// No `using Serilog` here either.
public MyService
{
private readonly ILogger log;
[ActivatorUtilitiesConstructor]
public MyService( ILogger<MyService> log )
{
this.log = log ?? throw new ArgumentNullException(nameof(log));
}
public MyService()
: this( log: NullLogger<MyService>.Instance )
{
}
public void Foo( String name )
{
this.log.LogInformation( "hello {Name}", name );
}
}
So this will just-work (though when using NullLogger<T> specifically, nothing will be logged, so that may-or-may-not be desirable):
void Bar()
{
MyService svc = new MyService();
svc.Foo();
}
Related
While I know how to inject dependencies using IoC container through constructors, I am having a hard time grasping the proper way (without using anti-patterns) of injecting new/more services at runtime.
Lets say I've got following code (simplified, but I actually have similar implementation and want to refactor it to use DI):
class ClassA : IClassA
{
public ClassA(ILogger<ClassA> logger)
...
public void Foo()
{
while(true)
{
var classA = new ClassA();
...
}
}
...
}
I register the services with Host:
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
...
services.AddScoped<IClassA, ClassA>();
...
});
I've read a lot of articles on the web and MS documentation and worked out that I can resolve ClassA in the while loop by:
1. Using IServiceScopeFactory (after injecting it into ClassA):
using IServiceScope scope = serviceScopeFactory.CreateScope();
var classA = scope.ServiceProvider.GetRequiredService<IClassA>();
This approach however, seems to be regarded as anti-pattern (not sure why since the dependencies are still being resolved by the IoC container and Microsoft presents this in documentation)
2. I can wrap above in the factory class, register it with the container, inject into ClassA and return the service from there:
var classA = classAFactory.GetClassA();
In factory class I would still be using IServiceScopeFactory and will have to create additional factory class : interface pair.
3. Create factory class as above but instead of using IServiceScopeFactory inside factory, do:
return new ClassA();
Which of these approaches should I prefer, or is there other, better pattern to request services in a loop/at runtime?
I'm writing a custom middleware for ASP.NET Core 2.2. According to Microsoft Docs on writing custom middlewares:
Middleware components can resolve their dependencies from dependency injection (DI) through constructor parameters. UseMiddleware<T> can also accept additional parameters directly.
This seems all good, but it doesn't say what happens when I mix the two ways, e.g. use DI and pass parameters in UseMiddleware<T>. For example, I have the following middleware:
public class CustomMiddleware
{
public CustomMiddleware(RequestDelegate next, ILogger<CustomMiddleware> logger, CustomMiddlewareOptions options)
{
...
}
public async Task InvokeAsync(HttpContext context)
{
...
}
where logger is provided by DI and options is provided like the following:
app.UseMiddleware<CustomMiddleware>(new CustomMiddlewareOptions());
My own testing with 2.2 seems to show that this works fine, and the order of the parameters in the constructor doesn't matter (I can place DI parameter before or after manually-passed parameter, or even in between two manually-passed parameters). But I'm looking for some assurances that what I'm doing is OK. It would be really great if anyone could point to some docs or source code that supports this sort of usage. Thanks!
My own testing with 2.2 seems to show that this works fine, and the order of the parameters in the constructor doesn't matter (I can place DI parameter before or after manually-passed parameter, or even in between two manually-passed parameters). But I'm looking for some assurances
Yes. After reading the source code, I would say it is fine.
How it works
Your CustomMiddleware is a by-convention Middleware (different from the Factory-based middleware), which is activated by ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs):
var ctorArgs = new object[args.Length + 1];
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length); //
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
Here the the args (Given Arguments) is the argument array that you pass into the UseMiddleware<CustomMiddleware>(args) (without the next).
And there're two stages when preparing the constructor arguments:
Match the given args against the construct parameter types. And set values when the type matches. See source code Here
Fill the null element using ServiceProvider.GetRequiredService<SomeService>().See source code here. If the service instance is still null, then use the default value.
For example, let's say :
You have a by-convention middleware whose constructor has the following signature:
public CustomMiddleware(RequestDelegate next, A a, B b, C c, D d, E e){ ... }
And then we pass in two arguments when registering the middleware :
app.UseMiddleware(c, a)
where c is an instance of C Type, and a is an instance of A Type. So the givenParameters Array is [next,c, a]
To create an instance of CustomMiddleware, the compiler needs to know the complete constructor parameter values. DI extension gets this constructor parameter values array (_parameterValues) within two stages.See :
The stage2 works in a way like below:
b'= sp.GetService(B);
if b' == null :
b' = default value of B
As you can see above, the ActivatorUtilities.CreateInstance(sp,mw,args) API deals with the order and missing arguments automatically.
As a side note, the by-convention middlewares are activated at startup-time and will always be a singleton. If you want to use scoped service, see this thread
But I'm looking for some assurances that what I'm doing is OK.
Well, this is opinion-based. While everything is working in your case it is okay, I believe. But I would prefere using options pattern introduced in ASP.NET Core.
public class CustomMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CustomMiddleware> _logger;
private readonly CustomMiddlewareOptions _options;
public CustomMiddleware(RequestDelegate next, ILogger<CustomMiddleware> logger, IOptions<CustomMiddlewareOptions> options)
{
_next = next;
_logger = logger;
_options = options.Value;
}
//...
}
For this one you will need to configure CustomMiddlewareOptions in Startup
services.Configure<CustomMiddlewareOptions>(options =>
{
options.Id = 1;
options.Name = "options";
//set other properties
});
In this case you should add middleware without parameters
app.UseMiddleware<CustomMiddleware>();
Note
It's also worth to take a look at RequestLocalizationMiddleware
public RequestLocalizationMiddleware(RequestDelegate next, IOptions<RequestLocalizationOptions> options)
and how framework offers you to use the middleware in ApplicationBuilderExtensions
public static IApplicationBuilder UseRequestLocalization(this IApplicationBuilder app)
{
//omitted
return app.UseMiddleware<RequestLocalizationMiddleware>();
}
or
public static IApplicationBuilder UseRequestLocalization(this IApplicationBuilder app, RequestLocalizationOptions options)
{
//omitted
return app.UseMiddleware<RequestLocalizationMiddleware>(Options.Create(options));
}
As you can see ASP.NET Core developers also prefer to stick with IOptions in middleware constructor and while manually specifying additional parameter they just wrap them into Options
I'm currently playing around with the IoC concept (with a WPF app) and I haven't decided on the tool I'll used with it just yet as I'm still trying to get the grasp of it but I'm confused as to how this would be configured regarding the specific parameters each component.
I understand how you define the relevant library in the config file and how it will determine which one should be used by the app and what its lifespan should be but what about each library requiring its own specific set of parameters.
Where do you get these from and when do you pass them on?
Taking your typical logger for example.
I have the following interface:
public interface ILogger
{
void Write(string message);
}
I have the logger class itself:
public class Logger : ILogger
{
private readonly ILogger _logger;
public Logger (ILogger logger)
{
_logger = logger;
}
public void Write(string message)
{
_logger.Write(message);
}
}
I then define multiple loggers each requiring their own parameter, so I implemented the following:
a) database logger: where a connection string is required so that I can log my message to a database.
public void LoggerDb: ILogger
{
public void Write(string message)
{
}
public ConnectionString {get; set;}
}
b) file logger: where a filename is required so that I can log my message to the relevant log file.
public void LoggerFile: ILogger
{
public void Write(string message)
{
}
public Filename {get; set;}
}
c) console logger: where no parameter is required as I just want to output my message to a console window.
public void LoggerConsole: ILogger
{
public void Write(string message)
{
}
}
In my console test app, I've got the following code in the Program.cs:
static void Main(string[] args)
{
string logTypeId = "d";
ILogger logType;
if (logTypeId == "d")
{
logType = new LoggerDb("Data Source=....");
}
else if (logTypeId == "f"
{
logType = new LoggerFile("c:\\mylog.txt");
}
else
{
logType = new LoggerConsole();
}
Logger logger = new Logger(logType);
logger.Write("Message 1");
logger.Write("Message 2");
logger.Write("Message 3");
}
I understand this is not how the code would be if I used an IoC tool. I'm just trying to highlight what I'm trying to achieve and I'm trying to get answers to the following questions:
Can this be achieved using an IoC tool i.e. pass specific parameter depending on the logger type that's used/defined in the IoC section of the app.config?
Is this the correct approach i.e. Having specific loggers with their own constructors parameters? If not, can you explain why and what should be the correct approach. I don't mind the IoC tool you use. I just want to understand how this should be done.
Where should these additional parameters be stored in the app.config?
First, note that in order to implement DI via an IoC, it is by no means required to configure your container in a configuration file (although it's certainly an option and many containers support it).
Most IoC containers these days also allow you to specify your setup in code. So I guess the answer is: it really depends on the IoC container you plan to use. My opinion: avoid xml-based configuration if you can; it's often a pain to maintain and brings little value if you ask me. In your code-based configuration you can still refer to configuration parameters from app.config or other.
You can also turn the question around: is it a requirement to have the container configuration in a separate file (and why)? If yes, look for a container that supports this well. But most do.
Some examples of configuration using a code-based DSL:
Autofac modules: http://docs.autofac.org/en/latest/configuration/modules.html
StructureMap: http://structuremap.github.io/registration/registry-dsl/
Some examples of xml configuration:
Autofac: http://docs.autofac.org/en/latest/configuration/xml.html
Spring.NET container: http://www.springframework.net/doc-latest/reference/html/objects.html
structuremap: http://docs.structuremap.net/configuring-structuremap/structuremap-xml-configuration/
It depends ;)
I can't speak for all DependencyInjection Tools, but many of them should support this functionality.
I don't see anything that speak against this. If you want to call different Loggers explicitly, you can do this. But you can also use some kind of LogListeners. One for DB, one for File and so on. And your Logger just delegates the LogMessage to all Loggers. But this depends on what you want or need ;)
This also depends on the implementation of the Logger. It's common to store the ConnectionString in the config. The other parameters are too specific, but you you can store them in config, too.
When using LibLog, is it possible to assert calls to the logger? Given the wiki lists the following example for usage:
public class MyClass
{
private static readonly ILog Logger = LogProvider.For<MyClass>();
}
Here the logger is an implementation detail hidden from the consumer, which is most of the benefit of using this library. Such that the library consumer does not have to worry about how loggers are instantiated. Looking at this blog post:
http://dhickey.ie/2015/06/capturing-log-output-in-tests-with-xunit2/
It seems that a lot of boiler plate is added to capture the log output, I'm not entirely sure about the approach, given that it also uses a redirected Serilog output in the unit test, something that seems odd given the library should only rely on the logging abstraction?
The only options I can currently think of are:
Inject the logger - This probably would be odd for the consumer of the library, and each library then would carry it's own ILogger definition that needs to be injected, defeating the advantages of the abstraction.
Wire up to a real logging framework - Set the current LogProvider for LibLog to use Log4Net or similar, and then somehow try and inject a mock / stub Logger into Log4Net, and assert calls via proxy.
Any relatively simple way to assert calls to the logger would be appreciated, but I suspect parallel test execution would cause problems even if it was possible to assert calls on the above logger?
In the logging config for almost all loggers you can configure then to throw exception when log fail.
Sample from nlog
<nlog throwExceptions="true">
... your nlog config
</nlog>
But in the abstraction created by LibLog you lost this features
What I've done in my project:
I've created my LoggerFactory. It exposes same static methods as NLogger.
public class LoggerFactory
{
private static ILoggerFactoryStrategy _loggerFactoryStrategy = new DummyLoggerFactoryStrategy();
public static void Initialize(ILoggerFactoryStrategy loggerFactoryStrategy)
{
_loggerFactoryStrategy = loggerFactoryStrategy;
}
public ILogger GetLogger<T>()
{
return _loggerFactoryStrategy.GetLogger<T>();
}
....
}
Dummy strategy can write just to debug output or do nothing. Another strategy could look smth like:
public class LoggerFactoryStrategy : ILoggerFactoryStrategy
{
public ILogger GetLogger<T>()
{
//create LibLog instance instead with LogProvider.For<T>()
var nlogger = LogManager.GetLogger(typeof(T).Name); //create instance of NLogger
return new NLogLogger(nlogger);
}
}
And NlogLogger wrapper could be smth like
internal class NLogLogger : ILogger
{
private readonly Logger _logger;
public NLogLogger(Logger logger)
{
_logger = logger;
}
public void Debug(string message)
{
_logger.Debug(message);
}
public void Warn(string message, params object[] args)
{
_logger.Warn(message, args);
}
public void Info(Exception exception)
{
_logger.Info(exception);
}
......
}
When application starts I initialize it with proper strategy what uses NLogger under the hood.
If I want to test calls to logger I can use mocked strategy.
This approach lets you to remove references to logger library across your solution, except your root projects and lets you switch from one to another if you need in the future.
Also, this allowed us to use NLogger in PCL projects.
How can I inject different implementation of object for a specific class?
For example, in Unity, I can define two implementations of IRepository
container.RegisterType<IRepository, TestSuiteRepositor("TestSuiteRepository");
container.RegisterType<IRepository, BaseRepository>();
and call the needed implementation
public BaselineManager([Dependency("TestSuiteRepository")]IRepository repository)
As #Tseng pointed, there is no built-in solution for named binding. However using factory method may be helpful for your case. Example should be something like below:
Create a repository resolver:
public interface IRepositoryResolver
{
IRepository GetRepositoryByName(string name);
}
public class RepositoryResolver : IRepositoryResolver
{
private readonly IServiceProvider _serviceProvider;
public RepositoryResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IRepository GetRepositoryByName(string name)
{
if(name == "TestSuiteRepository")
return _serviceProvider.GetService<TestSuiteRepositor>();
//... other condition
else
return _serviceProvider.GetService<BaseRepositor>();
}
}
Register needed services in ConfigureServices.cs
services.AddSingleton<IRepositoryResolver, RepositoryResolver>();
services.AddTransient<TestSuiteRepository>();
services.AddTransient<BaseRepository>();
Finally use it in any class:
public class BaselineManager
{
private readonly IRepository _repository;
public BaselineManager(IRepositoryResolver repositoryResolver)
{
_repository = repositoryResolver.GetRepositoryByName("TestSuiteRepository");
}
}
In addition to #adem-caglin answer I'd like to post here some reusable code I've created for name-based registrations.
UPDATE Now it's available as nuget package.
In order to register your services you'll need to add following code to your Startup class:
services.AddTransient<ServiceA>();
services.AddTransient<ServiceB>();
services.AddTransient<ServiceC>();
services.AddByName<IService>()
.Add<ServiceA>("key1")
.Add<ServiceB>("key2")
.Add<ServiceC>("key3")
.Build();
Then you can use it via IServiceByNameFactory interface:
public AccountController(IServiceByNameFactory<IService> factory) {
_service = factory.GetByName("key2");
}
Or you can use factory registration to keep the client code clean (which I prefer)
_container.AddScoped<AccountController>(s => new AccountController(s.GetByName<IService>("key2")));
Full code of the extension is in github.
You can't with the built-in ASP.NET Core IoC container.
This is by design. The built-in container is intentionally kept simple and easily extensible, so you can plug third-party containers in if you need more features.
You have to use a third-party container to do this, like Autofac (see docs).
public BaselineManager([WithKey("TestSuiteRepository")]IRepository repository)
After having read the official documentation for dependency injection, I don't think you can do it in this way.
But the question I have is: do you need these two implementations at the same time? Because if you don't, you can create multiple environments through environment variables and have specific functionality in the Startup class based on the current environment, or even create multiple Startup{EnvironmentName} classes.
When an ASP.NET Core application starts, the Startup class is used to bootstrap the application, load its configuration settings, etc. (learn more about ASP.NET startup). However, if a class exists named Startup{EnvironmentName} (for example StartupDevelopment), and the ASPNETCORE_ENVIRONMENT environment variable matches that name, then that Startup class is used instead. Thus, you could configure Startup for development, but have a separate StartupProduction that would be used when the app is run in production. Or vice versa.
I also wrote an article about injecting dependencies from a JSON file so you don't have to recompile the entire application every time you want to switch between implementations. Basically, you keep a JSON array with services like this:
"services": [
{
"serviceType": "ITest",
"implementationType": "Test",
"lifetime": "Transient"
}
]
Then you can modify the desired implementation in this file and not have to recompile or change environment variables.
Hope this helps!
First up, this is probably still a bad idea. What you're trying to achieve is a separation between how the dependencies are used and how they are defined. But you want to work with the dependency injection framework, instead of against it. Avoiding the poor discover-ability of the service locator anti-pattern. Why not use generics in a similar way to ILogger<T> / IOptions<T>?
public BaselineManager(RepositoryMapping<BaselineManager> repository){
_repository = repository.Repository;
}
public class RepositoryMapping<T>{
private IServiceProvider _provider;
private Type _implementationType;
public RepositoryMapping(IServiceProvider provider, Type implementationType){
_provider = provider;
_implementationType = implementationType;
}
public IRepository Repository => (IRepository)_provider.GetService(_implementationType);
}
public static IServiceCollection MapRepository<T,R>(this IServiceCollection services) where R : IRepository =>
services.AddTransient(p => new RepositoryMapping<T>(p, typeof(R)));
services.AddScoped<BaselineManager>();
services.MapRepository<BaselineManager, BaseRepository>();
Since .net core 3, a validation error should be raised if you have failed to define a mapping.