In the Act phase, we call the Execute method of our service. This method logs a line to the ILogger implementation that is injected upon instantiation. Then, we assert that the line was written in the lines list (that’s what AssertableLogger does; it writes to a List<string>). In an ASP.NET Core application, all that logging goes to the console by default. Logging is a great way to know what is happening in the background when running the application.The Service class is a simple consumer of an ILogger<Service>. You can do the same for any class you want to add logging support to. Change Service by that class name to have a logger configured for your class. That generic argument becomes the logger’s category name when writing log entries.Since ASP.NET Core uses a WebApplication instead of a generic IHost, here is the same test code using that construct:
[Fact]
public void Should_log_the_Service_Execute_line_using_WebApplication()
{
// Arrange
var lines = new List<string>();
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders()
.AddAssertableLogger(lines);
builder.Services.AddSingleton<IService, Service>();
var app = builder.Build();
var service = app.Services.GetRequiredService<IService>();
// Act
service.Execute();
// Assert
Assert.Collection(lines,
line => Assert.Equal(“Service.Execute()”, line)
);
}
I highlighted the changes in the preceding code. In a nutshell, the extension methods used on the generic host have been replaced by WebApplicationBuilder properties like Logging and Services. Finally, the Create method creates a WebApplication instead of an IHost, exactly like in the Program.cs file.To wrap this up, these test cases allowed us to implement the most commonly used logging pattern in ASP.NET Core and add a custom provider to ensure we logged the correct information. Logging is essential and adds visibility to production systems. Without logs, you don’t know what is happening in your system unless you are the only one using it, which is very unlikely. You can also log what is happening in your infrastructure and run real-time security analysis on those log streams to quickly identify security breaches, intrusion attempts, or system failures. These subjects are out of the scope of this book, but having strong logging capabilities at the application level can only help your overall logging strategy.Before moving on to the next subject, let’s explore an example that leverages the ILoggerFactory interface. The code sets a custom category name and uses the created ILogger instance to log a message. This is very similar to the previous example. Here’s the whole code:
namespace Logging;
public class LoggerFactoryExploration
{
private readonly ITestOutputHelper _output;
public LoggerFactoryExploration(ITestOutputHelper output)
{
_output = output ??
throw new ArgumentNullException(nameof(output));
}
[Fact]
public void Create_a_ILoggerFactory()
{
// Arrange
var lines = new List<string>();
var host = Host.CreateDefaultBuilder()
.ConfigureLogging(loggingBuilder => loggingBuilder
.AddAssertableLogger(lines)
.AddxUnitTestOutput(_output))
.ConfigureServices(services => services.AddSingleton<Service>())
.Build()
;
var service = host.Services.GetRequiredService<Service>();
// Act
service.Execute();
// Assert
Assert.Collection(lines,
line => Assert.Equal(“LogInformation like any ILogger<T>.”, line)
);
}
public class Service
{
private readonly ILogger _logger;
public Service(ILoggerFactory loggerFactory)
{
ArgumentNullException.ThrowIfNull(loggerFactory);
_logger = loggerFactory.CreateLogger(“My Service”);
}
public void Execute()
{
_logger.LogInformation(“LogInformation like any ILogger<T>.”);
}
}
}
The preceding code should look very familiar. Let’s focus on the highlighted lines, which relate to the current pattern:
- We inject the ILoggerFactory interface into the Service class constructor (instead of an ILogger<Service>).
- We create an ILogger instance with the ” My Service” category name.
- We assign the logger to the _logger field.
- We then use that ILogger from the Execute method.
As a rule of thumb, I recommend using the ILogger<T> interface by default. If impossible, or if you need a more dynamic way of setting the category name for your log entries, leverage the ILoggerFactory instead. By default, when using ILogger<T>, the category name is the T parameter, which should be the name of the class creating log entries. The ILoggerFactory interface is more of an internal piece than something made for us to consume; nonetheless, it exists and satisfies some use cases.
Note
In the preceding example, the ITestOutputHelper interface is part of the Xunit.Abstractions assembly. It allows us to write lines as standard output to the test log. That output is available in the Visual Studio Test Explorer.
Now that we have covered how to write log entries, it’s time to learn how to manage their severity.