Reputation: 82437
I have the code like this in my unit test setup:
IServiceCollection services = new ServiceCollection();
services.AddSingleton<IDependency, Dependency>();
services.AddLogging();
services.RemoveAll<ILoggerProvider>();
services.AddSingleton<ILoggerProvider, MyCustomProvider>(provider =>
{
var myDependency = provider.GetService<IDependency>();
return new MyCustomProvider(myDependency );
});
var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetService<ILogger>();
When I run this logger
is null.
The lambda for the DI factory to create MyCustomProvider
is never called. Nor is the constructor to MyCustomProvider
nor the constructor of the class I made that implements ILogger
.
I am guessing that I am missing a step to wire this up. But there is a lot of conflicting documentation out there between .Net Core 1, 2 and 3.
What do I need to do to wire up my provider the .Net Core 3 way? (Such that I can get an ILogger
that uses it.)
NOTES:
LoggingBuilder.AddProvider
. But if you look at the source code for that extension, it just calls AddSingleton
, which is what I do, but with a factory. (Got the idea here.)services.RemoveAll<ILoggerProvider>()
is the same as calling ClearProviders
.For Example:
public class SomeClass
{
public SomeClass(ILogger<SomeClass> logger)
{
// Do Stuff
}
}
Somehow this is done without indicating any provider. I would like to make it be the same in my unit test. (Just use my custom provider.)
Upvotes: 4
Views: 1810
Reputation: 12779
The reason your logger
variable is null
in the example is because ILogger
isn't actually registered as a service. If you look at the source of AddLogging
you can see it only registers ILogger<>
and ILoggerFactory
. If you've ever tried to accept an ILogger
instead of an ILogger<MyClass>
via the .NET Core DI you will have run into the following exception*:
System.InvalidOperationException: 'Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger' while attempting to activate 'Your.Service'
Based on this, your testing code is flawed as you'd never have received an ILogger
in the first place. To see that you're code is in fact working, modify your testing code so that it retrieves an ILogger<SomeClass>
instead. The result of your variable will be non-null and a break point set in your provider's constructor will be hit:
// Get*Required*Service won't throw
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
If you want to be able to inject ILogger
, you will need to register it separately with a default category name**:
services.AddSingleton<ILoggerProvider, MyCustomProvider>(); // no need for lambda
services.AddSingleton<ILogger>(sp =>
sp.GetService<ILoggerFactory>().CreateLogger("Default")
);
The following will both now work and use your custom provider:
var loggerA = serviceProvider.GetRequiredService<ILogger<Program>>();
var loggerB = serviceProvider.GetRequiredService<ILogger>();
I have my own custom provider/factory/logger that I use to inject xUnit's ITestOutputHelper
into a custom logger following the same registration pattern as you, so I know from personal experience that this works. But I've also tested that your specific code is functional (mocking out my own IDependency
)--the above code is executed and breakpoints set in the constructor of MyCustomProvider
and the service registration are hit. Additionally, if I inject the logger into a class it's hit as well (as expected).
Your comment "Somehow this is done without indicating any provider" is misinformed, because you do in fact have a provider registered! But even in a scenario where all providers were cleared and no new ones were added, you'd still get a non-null logger. This is because LoggerFactory
just loops through each provider to build up a new logger wrapper around them. If there's no providers then you essentially get a no-op logger.
* This is unfortunate as some tools (like R#) will suggest converting the parameter to the base type ILogger
which then breaks DI!
** A default category name is required since there is no generic type argument to pass to ILoggerFactory.CreateLogger
. For the generic ILogger<T>
the category name is always a variation of T
's name--but we obviously don't get that with the non-generic version. If I had to guess, this is probably why they don't register an implementation of ILogger
by default.
Upvotes: 3
Reputation: 2166
It may be a shot in the dark, but you can try adding your ILoggerProvider
to the ILoggerFactory
:
var loggerFactory = serviceProvider.GetService<ILoggerFactory();
var loggerProvider = serviceProvider.GetService<ILoggerProvider>();
loggerFactory.AddProvider(loggerProvider);
In case it was constructed sooner than you have registered your provider, it might not be aware of it's existance.
Alternatively, would it work, if you also tell it, how to resolve the ILogger
?:
...
services.AddSingleton<ILoggerProvider, MyCustomProvider>(provider =>
{
var myDependency = provider.GetService<IDependency>();
return new MyCustomProvider(myDependency );
});
services.AddSingleton(typeof(ILogger<>), provider =>
{
var loggerProvider = provider.GetService<ILoggerProvider>();
return loggerProvider.CreateLogger("TestLogger");
});
...
Finally... is there a reason, why you can't just inject ILoggerProvider
into your tests, and call loggerProvider.CreateLogger(<test_class_name>)
to get the logger?
Upvotes: 0
Reputation: 3778
NET CORE Testing I'm doing like this:
in the constructor of the test class (or where you do your DI mapping)
public class UnitTest1
{
public IConfigurationRoot Configuration { get; set; }
private readonly IDisponibilitaService _disponibilitaService;
public UnitTest1()
{
var services = new ServiceCollection();
Configuration = new ConfigurationBuilder()
.SetBasePath(Path.Combine(Path.DirectorySeparatorChar.ToString(), "directory", "Kalliope_CTI", "backend", "KalliopeCTITestProject"))
.AddJsonFile("testconfig.json", optional: false, reloadOnChange: true)
.Build();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>(); //< -- HERE TRY TO SET NullLoggerFactory
var serviceProvider = services
.AddOptions()
.BuildServiceProvider();
}
//// your test methods
}
Hope it helps you!!
Upvotes: 0