I'm writing code for background service which is designed to collect data by OPC UA protocol. Service worked as intended, before I desided to refactor in order to conform to modern patterns (DI).
Before refactor the main part of the service (while loop routine), I tried to refactor my FileLogger to be used as a service, so as it is constructor-injected to my hosted service.
The problem arose when I try to register services in container. Firstly I hardcoded the path to log directory (first iteration of worker used @$"{System.AppContext.BaseDirectory}\logs"
path) when passing it to the constructor but it failed to start.
I put a block of code into StartAsync
method:
using (StreamWriter writer = new("path to file", true)) {
await writer.WriteLineAsync(message);
}
There is no file created and my service is out of game. It seems like StartAsync
isn't started at all and unhandled exception is thrown on host creation.
The structure of the file logging service:
using System.Globalization;
using System.Text.RegularExpressions;
namespace OpcService.Services;
internal interface IOpcLogger
{
Task<bool> AppendMessage(string message, DateTime timestamp);
Task<bool> AppendMessageList(List<string> messages, DateTime timestamp);
bool DeleteMessagesBeforeToday(byte daysBeforeToday);
}
internal class OpcFileLogger : IOpcLogger
{
private const string TIMESTAMP_FORMAT = "HH:mm:ss";
public string MessageDirectory { get; }
public OpcFileLogger(string messageDirectory)
=> MessageDirectory = (string.IsNullOrWhiteSpace(messageDirectory))
? messageDirectory
: throw new ArgumentException($"{nameof(messageDirectory)} cannot be null/empty/whitespace.");
public async Task<bool> AppendMessage(string message, DateTime timestamp)
{
try {
if (string.IsNullOrWhiteSpace(message)) {
throw new ArgumentException($"{nameof(message)} cannot be null/empty/whitespace.", nameof(message));
}
if (!Directory.Exists(MessageDirectory)) {
Directory.CreateDirectory(MessageDirectory);
}
message = $"[{timestamp.ToString(TIMESTAMP_FORMAT)}] {message}";
using (StreamWriter writer = new(@$"{MessageDirectory}\{timestamp.ToString("ddMMyyyy")}.txt", true)) {
await writer.WriteLineAsync(message);
}
return true;
} catch {
return false;
}
}
// other methods
}
Dependent class:
internal class OpcUaWorker : BackgroundService
{
private WorkerSettings? ServiceSettings { get; set; }
private readonly IOpcLogger _opcLogger;
private string WorkerFilePath { get; } = @$"{System.AppContext.BaseDirectory}\worker.json";
private string DBConfigFilePath { get; } = @$"{System.AppContext.BaseDirectory}\db.config";
// private string LogsDirectoryPath { get; } = @$"{System.AppContext.BaseDirectory}\logs";
public OpcUaWorker(IOpcLogger opcLogger)
=> _opcLogger = opcLogger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// opc session
}
public override async Task StartAsync(CancellationToken stoppingToken)
{
await _opcLogger.AppendMessage("Module is starting...\n", DateTime.Now);
await base.StartAsync(stoppingToken);
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
await _opcLogger.AppendMessage("Module is stopping...\n", DateTime.Now);
await base.StopAsync(stoppingToken);
}
}
Injector routine:
using OpcService;
using OpcService.Services;
IHost host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices(services => {
services.AddSingleton<IOpcLogger, OpcFileLogger>(provider => new OpcFileLogger(@"d:\Projects\OpcUaWorker\logs"));
services.AddHostedService<OpcUaWorker>();
})
.Build();
await host.RunAsync();
Only when use services.AddSingleton<IOpcLogger, OpcFileLogger>();
and don't use parameterised constructor (public string MessageDirectory { get; } = @"d:\Projects\OpcUaWorker\logs";
), it works.
As recommended in comments, I find the following error record in Event View:
Application: OpcUaWorker.exe
CoreCLR Version: 6.0.1122.52304
.NET Version: 6.0.11
Description: The process was terminated due to an unhandled exception.
Exception Info: System.InvalidOperationException: Unable to resolve service for type 'OpcService.Services.IOpcLogger' while attempting to activate 'OpcService.OpcUaWorker'.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites(ServiceIdentifier serviceIdentifier, Type implementationType, CallSiteChain callSiteChain, ParameterInfo[] parameters, Boolean throwIfCallSiteNotFound)
I made up the solution with the help of recommendation in the comment section by looking at Event Viewer. After I tried to debug my code using VSCode without await host.RunAsync();
(another tip from the same user), I realised that my object wasn't initalized with correct argument, so taking look at OpcUaWorker
constructor led me to:
public OpcFileLogger(string messageDirectory)
=> MessageDirectory = (string.IsNullOrWhiteSpace(messageDirectory))
? throw new ArgumentException($"{nameof(messageDirectory)} cannot be null/empty/whitespace.")
: messageDirectory;