2

I've created a logger which should log anything during start of application run. And I wanted it to persist between Startup and ConfigureServices. I store it in the property similar to configuration

Here is the code


public Startup(IConfiguration configuration)
{
    Configuration = configuration;

    using var loggerFactory = LoggerFactory.Create(builder =>
    {
        builder.SetMinimumLevel(LogLevel.Error);
        builder.AddEventLog(s =>
        {
            s.LogName = "MyLogName";
            s.SourceName = "MySource";
        });

        // add some other persisting logger
    });
    StartupLogger = loggerFactory.CreateLogger<Startup>();
}

public IConfiguration Configuration { get; }

public ILogger StartupLogger { get; } // Logger exists here when breakpoint is in ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
        
    StartupLogger.LogError("Failed to do something"); // <-- throws exception
}

This is the error I get. Looks like inner logger gets disposed in the process

Message: "An error occurred while writing to logger(s). (Cannot access a disposed object. Object name: 'EventLogInternal'.

Full stack

'StartupLogger.LogError("Failed to do something")' threw an exception of type 'System.AggregateException'
Data: {System.Collections.ListDictionaryInternal}
HResult: -2146233088
HelpLink: null
InnerException: {"Cannot access a disposed object.\r\nObject name: 'EventLogInternal'."}
InnerExceptions: Count = 1
Message: "An error occurred while writing to logger(s). (Cannot access a disposed object.\r\nObject name: 'EventLogInternal'.)"
Source: "Microsoft.Extensions.Logging"
StackTrace: " at Microsoft.Extensions.Logging.Logger.ThrowLoggingError(List`1 exceptions)\r\n at Microsoft.Extensions.Logging.Logger.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter)\r\n at Microsoft.Extensions.Logging.Logger`1.Microsoft.Extensions.Logging.ILogger.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter)\r\n at Microsoft.Extensions.Logging.LoggerExtensions.Log(ILogger logger, LogLevel logLevel, EventId eventId, Exception exception, String message, Object[] args)\r\n at Microsoft.Extensions.Logging.LoggerExtensions.LogError(ILogger logger, String message, Object[] args)"
TargetSite: {Void ThrowLoggingError(System.Collections.Generic.List`1[System.Exception])}

Surely I have a wrong approach here. Appreciate some guidance.

T.S.
  • 18,195
  • 11
  • 58
  • 78
  • I'm thinking that when the logger factory gets disposed at the end of the `Startup` constructor, it disposes any associated loggers. It's the only logical explanation that I can see. – ProgrammingLlama Jun 30 '22 at 03:42
  • 2
    Why do you add `using` to the variable declaration, when you want it to persist outside of the scope of the method? – gunr2171 Jun 30 '22 at 03:45
  • @gunr2171 good eye. Gonna try this now. May be I copied from some example. don't remember – T.S. Jun 30 '22 at 03:46
  • 1
    Do you really need to log within `ConfigureServices`? Or do you need to log the values you are assigning to classes that follow the options pattern? https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-6.0 – Jeremy Lakeman Jun 30 '22 at 03:49
  • @JeremyLakeman in this `ConfigureServices` I have a lot of custom initialization stuff. Reading custom settings values and decrypting them. dynamically wire dependency injection and more. Custom authentication wire up. etc – T.S. Jun 30 '22 at 03:57

2 Answers2

4

Change the using declaration using var loggerFactory to the ordinary one - it leads to the disposal of the loggerFactory at the end of scope (Startup constructor in this case):

public Startup(IConfiguration configuration)
{
    Configuration = configuration;

    var loggerFactory = LoggerFactory.Create(builder =>
    {
        builder.SetMinimumLevel(LogLevel.Error);
        builder.AddEventLog(s =>
        {
            s.LogName = "MyLogName";
            s.SourceName = "MySource";
        });

        // add some other persisting logger
    });
    StartupLogger = loggerFactory.CreateLogger<Startup>();
}

Also note that Startup Configure method supports dependency injection and if it feasible for you to move your initialization logic there - you can just inject ILogger<Startup> into this method:

public Startup(IConfiguration configuration)
{
    // ...
    public void Configure(IApplicationBuilder app, , IWebHostEnvironment env, ILogger<Startup> logger)
    {
        logger.LogError("Failed to do something"); 
    }
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • 1
    yes. This works. Blur vision. Not sure how I arrived to the `using` there. Must be some copy/paste. been weeks. Only when ran into "unhappy" scenario, it manifested – T.S. Jun 30 '22 at 03:54
  • 1
    @T.S. happens even to the best of us =) Also please see the update, JIC. – Guru Stron Jun 30 '22 at 03:56
  • good point on the DI on startup. Ha!!! I knew that I don't know something, So, the app will wire me up a logger per configurations in app settings/user secrets, correct? Will try right now! – T.S. Jun 30 '22 at 04:03
  • @T.S. yes, it should. – Guru Stron Jun 30 '22 at 04:04
  • no, it did not do it `System.InvalidOperationException: 'The ConfigureServices method must either be parameterless or take only one parameter of type IServiceCollection.'` – T.S. Jun 30 '22 at 04:07
  • 1
    @T.S. yes, my bad, it is supported only in `Configure` call. Is it suitable to move the initialization logic to `Configure`? – Guru Stron Jun 30 '22 at 04:13
  • 1
    Potentially. I will have to give it a try tomorrow. Depends what runs first. I'll need to research. Right now my code is working for check in. No show stopper. Your tips are greatly appreciated!! +1 – T.S. Jun 30 '22 at 04:16
  • @T.S. was glad to help! `ConfigureServices` runs first and then `Configure` before the app start. – Guru Stron Jun 30 '22 at 04:18
  • wait, `public Startup(IConfiguration configuration)` runs first. then `public void ConfigureServices(IServiceCollection services)` - this I know. – T.S. Jun 30 '22 at 04:20
  • 1
    @T.S. yes, `Startup` ctor will run first for sure, then `Startup.ConfigureServices` then `Startup.Configure`. – Guru Stron Jun 30 '22 at 04:21
  • 1
    phew... yes. The class is `Startup`. of course, this is a constructor. But may be not - I need services there for my initializations. I will need to see – T.S. Jun 30 '22 at 04:28
1

Do you really need to log within ConfigureServices? Or do you need to log the options you are updating?

For anything that follows the Options Pattern, (and all built in services do) you can configure the options in a lambda, injecting other services;

services.AddMvc();
services.AddOptions<MvcOptions>()
    .Configure<ILogger<Startup>>((options, logger) => {
        options.something = X;
        logger.LogInformation($"... {X} ...");
    });

Note that the services.AddMvc(options => {...}); extension method is internally doing something very similar.

Or perform configuration via dedicated services;

services.AddTransient<IConfigureOptions<MvcOptions>, ConfigureMvcOptions>();

public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>{
    private readonly ILogger<ConfigureMvcOptions> logger;
    public ConfigureMvcOptions(ILogger<ConfigureMvcOptions> logger){
        ...
    }
    
    public void Configure(MvcOptions config)
    {
        options.something = X;
        logger.LogInformation($"... {X} ...");
    }
}

Or perhaps one service for everything;

services.AddSingleton<ConfigureEverything>();
services.AddSingleton<IConfigureOptions<MvcOptions>>(p => p.GetService<ConfigureEverything>());
services.AddSingleton<IConfigureOptions<SomethingElse>>(p => p.GetService<ConfigureEverything>());
// etc

public class ConfigureMvcOptions : 
    IConfigureOptions<MvcOptions>,
    IConfigureOptions<SomethingElse>,
    // etc

Obviously this is less flexible that just creating a startup logger. As in all the above cases, the configuration methods are only called on first use, once the generic host is starting.

Jeremy Lakeman
  • 9,515
  • 25
  • 29
  • This is interesting!!! Today I solved my issue (removed using). And I will check this out tomorrow. Thank you – T.S. Jun 30 '22 at 04:13