junalmeida / autofac-azurefunctions

Autofac implementation of the dependency injection factory for non isolated Azure Functions.
MIT License
20 stars 6 forks source link

ILogger<T> not resolved in Azure Functions startup class using Autofac #29

Closed LockTar closed 1 year ago

LockTar commented 1 year ago

Hi,

First a small question Is it better to use the classes in the documentation of Autofac or are these classes already in the nuget package and you can skip the first part and go directly to configuring the startup class?

Then my real question I hope you have an idea on how to solve this because I'm already busy with this for hours. Duplicate of StackOverflow post

I'm trying to get instance of ILogger<T> when resolving a dependency in the Startup class of an Azure Function v4 in process version.

Only this doesn't work and will result in an exception, null or a not working logger.

I followed the following AutoFac documentation for Azure Functions.

I register some types and try to resolve a type in the Startup class. The problem is the ILogger<T>. In this case the Logger<EventBusServiceBus>.

In the sample I try to resolve IEventBus in the ConfigureEventBus method. Because of this, services.AddSingleton<IEventBus, EventBusServiceBus>(sp => ....); is trying to be resolved in the AddEventBus method.

I don't understand why it can't be resolved because I see some registered types in the Autofac container.

See the comments in the code.

Thanks!

Simplified Startup from the docs:

internal class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        // Use IServiceCollection.Add extension method to add features as needed, e.g.
        //builder.Services.AddDataProtection();

        builder.Services.AddSingleton(GetContainer(builder.Services));

        // Important: Use AddScoped so our Autofac lifetime scope gets disposed
        // when the function finishes executing
        builder.Services.AddScoped<LifetimeScopeWrapper>();

        builder.Services.Replace(ServiceDescriptor.Singleton(typeof(IJobActivator), typeof(AutofacJobActivator)));
        builder.Services.Replace(ServiceDescriptor.Singleton(typeof(IJobActivatorEx), typeof(AutofacJobActivator)));

        builder.Services.AddCustomIntegrations(configuration);
        builder.Services.AddEventBus(configuration);

        var serviceProvider = builder.Services.BuildServiceProvider();
        ConfigureEventBus(serviceProvider);
    }

    private static IContainer GetContainer(IServiceCollection serviceCollection)
    {
        var containerBuilder = new ContainerBuilder();
        containerBuilder.Populate(serviceCollection);
        containerBuilder.RegisterModule<LoggerModule>();

        // This is a convenient way to register all your function classes at once
        containerBuilder.RegisterAssemblyTypes(typeof(Startup).Assembly)
            .InNamespaceOf<Function1>();

        // TODO: Register other dependencies with the ContainerBuilder like normal

        return containerBuilder.Build();
    }

    private void ConfigureEventBus(ServiceProvider sp)
    {
        var eventBus = sp.GetRequiredService<BuildingBlocks.EventBus.Abstractions.IEventBus>();

        // Unable to use eventBus here
    }
}

Some extension methods:

static class CustomExtensionsMethods
{
    public static IServiceCollection AddCustomIntegrations(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSingleton<IServiceBusPersisterConnection>(sp =>
        {
            var serviceBusConnectionString = configuration["EventBusConnection"];

            var subscriptionClientName = configuration["SubscriptionClientName"];

            return new DefaultServiceBusPersisterConnection(serviceBusConnectionString);
        });

        return services;
    }

    public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
        {
            var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();

            // Get autofac container so we rolve dependencies from there
            var autoFacContainer = sp.GetRequiredService<IContainer>();

            // This doesn't work
            ////var iLifetimeScope = sp.GetRequiredService<ILifetimeScope>();

            // This does work
            var iLifetimeScope = autoFacContainer.Resolve<ILifetimeScope>();

            // This doesn't work
            //var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>();

            // This doesn't work either but at least now no error/exception. Resolves into empty logger.
            var loggerFactory = new LoggerFactory();
            var logger = loggerFactory.CreateLogger<EventBusServiceBus>();

            // Tried these for logging without luck.
            // Don't understand because I see a loggerfactory and Ilogger in the container...

            //var loggerFactory = autoFacContainer.ResolveNamed<ILoggerFactory>("loggerFactory");
            //var loggerFactory = autoFacContainer.Resolve<ILoggerFactory>();
            ////var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
            ////var logger = loggerFactory.CreateLogger<EventBusServiceBus>();

            ////autoFacContainer.Resolve<ILogger<EventBusServiceBus>>();

            var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
            string subscriptionName = configuration["SubscriptionClientName"];

            return new EventBusServiceBus(serviceBusPersisterConnection, logger,
                eventBusSubcriptionsManager, iLifetimeScope, subscriptionName);
        });

        services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>();

        return services;
    }
}
LockTar commented 1 year ago

So I did found a working solution for now. Maybe you have a better solution but this works for me for now...

public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration)
{
    services.AddSingleton<IEventBus, EventBusServiceBus>(sp =>
    {
        var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>();

        // Get autofac container so we resolve dependencies from there
        var autoFacContainer = sp.GetRequiredService<IContainer>();

        // This does work
        var iLifetimeScope = autoFacContainer.Resolve<ILifetimeScope>();

        // Create a new logger that is connected to the console with a minimum loglevel from host.json based on the namespace
        var loggerFactory = LoggerFactory.Create(builder =>
        {
            var logLevel = configuration.GetValue<LogLevel>("AzureFunctionsJobHost:logging:logLevel:" + typeof(Startup).Namespace, LogLevel.Information);

            builder.AddConsole();
            builder.SetMinimumLevel(logLevel);
        });
        var logger = loggerFactory.CreateLogger<EventBusServiceBus>();

        var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>();
        string subscriptionName = configuration["SubscriptionClientName"];

        return new EventBusServiceBus(serviceBusPersisterConnection, logger,
            eventBusSubcriptionsManager, iLifetimeScope, subscriptionName);
    });

    return services;
}

So the important part is this:

// Create a new logger that is connected to the console with a minimum loglevel from host.json based on the namespace
var loggerFactory = LoggerFactory.Create(builder =>
{
    var logLevel = configuration.GetValue<LogLevel>("AzureFunctionsJobHost:logging:logLevel:" + typeof(Startup).Namespace, LogLevel.Information);

    builder.AddConsole();
    builder.SetMinimumLevel(logLevel);
});
var logger = loggerFactory.CreateLogger<EventBusServiceBus>();
junalmeida commented 1 year ago

I think you don't need to create a separate LoggerFactory as this is supposed to be registered already. What I would try in your case, if you really need an instance of ILogger<T> is something like:

  builder.RegisterGeneric(typeof(Logger<>))
          .As(typeof(ILogger<>))
          .SingleInstance();

You can also use the plain ILogger that will work out-of-the-box.

As for using the documentation or the classes on this package, it is up to what you consider best for your project. The latest version of this package contains the classes suggested in the documentation.

junalmeida commented 1 year ago

If this autofac registration does not work, you can also do something like:

builder.Register(ctx => {
               var factory = ctx.Resolve<ILoggerFactory>();
               // create the logger here, (inspect ctx for T being requested) 
              return logger;
          })
          .As(typeof(ILogger<>))
          .SingleInstance();
LockTar commented 1 year ago

I will give it a try and will let you know. Thanks!

LockTar commented 1 year ago

I tried all kind of setups but I'm still getting issues with that logger. I don't understand why I just can't get an instance of that logger in de startup.cs. Now I got something working with the logger locally, in Azure I get the same errors again.

I think I'm gonna leave Functions. Every time there is something wrong with it. Isolated or in process, doesn't matter. I need to use durable functions so I'm stuck with in process. Durable isolated aren't stable (yet).

junalmeida commented 1 year ago

ok wait. So, for Isolated is a whole different thing, and you don't need this package. This package is meant for non-isolated projects. I use this extensively on dozens of durable functions with no problem. I'm not sure I got your problem this time. You said you can't get an instance in Startup.cs, however you only need an instance in function classes, no? And yet, you could use only ILogger instead of ILogger<T>. I don't think there is a much significant difference unless you have anything specific to your project and you may correct me there.

LockTar commented 1 year ago

ok wait. So, for Isolated is a whole different thing, and you don't need this package. This package is meant for non-isolated projects.

Sorry for the confusing. My project is not an isolated project but an in process version. What I meant was that er always something with Azure functions. It changes to much and logging is always an issue from v1 until v4 isolated mode. I need in process right now because durable functions aren't stable yet for Isolated.

You said you can't get an instance in Startup.cs, however you only need an instance in function classes, no?

I will explain a bit more on what I'm trying to do. I have several domains in a project. Some are Web API projects and some are Functions (Isolated) and some are Functions (in process) because durable functions are in example not stable in isolated. My DI is autofac. My Azure function in process needs to start and:

So the first bullet works fine but I get problems with the second. We use this project for reference and you will see a Startup.cs Web API sample here for reference.

The problem is line 338 and 339 (in my functions verison).

I need there the iLifetimeScope and the ILogger<[EventBusServiceBus>. All other resolving works.

This singleton services.AddSingleton<IEventBus, EventBusServiceBus>(sp => is resolved on startup because of method ConfigureEventBus. See that method here.

It resolves the registered EventBusServiceBus as IEventBus and tries to subscribe on events. Those events are Mediatr handlers that are running as "background processes". But it doesn't matter what I try, it crashes on the lines iLifetimeScope and the ILogger<[EventBusServiceBus>. I thought I had a solution but then in one of my handlers it crashed again on another ILogger. So there is definitely wrong with resolving the loggers.

I think it's because of the LoggerModule. It crashes there on sequence contains not matching elements. To be honest, I don't fully understand that part. I understand the functional part but not really the technical line here of registering with a named parameter like this lifetimeScope.Resolve<ILoggerFactory>( new NamedParameter(LoggerModule.LoggerFactoryParam, loggerFactory) ); Because I tried to resolve with name without luck.

And yet, you could use only ILogger instead of ILogger<T>.

Could you explain a bit more on how to get in example that ILogger then? Because I spend way to much hours on this already and I don't see it anymore. Thanks!

junalmeida commented 1 year ago

The problem is line 338 and 339 (in my functions verison).

I don't understand why do you need to mix two dep injection libraries. Here I see .AddSingleton, which is the native DI, and inside it I see you requiring ILifetimeScope which is Autofac. Why don't you have a regular Autofac ContainerBuilder doing that?

Could you explain a bit more on how to get in example that ILogger then?

This is supposed to be like

public class MyFunction {
    public MyFunction(ILogger logger) { // ILogger as opposed to ILogger<EventBusServiceBus>
    // ...
    }

or

scope.Resolve<ILogger>();

Unless you have a specific reason to need ILogger<T>

junalmeida commented 1 year ago

See https://github.com/junalmeida/autofac-azurefunctions/blob/ca23a6af6ecfaae64e9133a175d221b8048d2d0d/SampleAutofacFunction/Services/Service1.cs#L12

junalmeida commented 1 year ago

BTW the ILogger is registered per instance. So, you need a function trigger scope to Resolve it. You can't resolve ILogger inside a singleton scope.

LockTar commented 1 year ago

I don't understand why do you need to mix two dep injection libraries. Here I see .AddSingleton, which is the native DI, and inside it I see you requiring ILifetimeScope which is Autofac. Why don't you have a regular Autofac ContainerBuilder doing that?

I don't understand either why Microsoft is doing that in the sample app so I rewrote it to the code as below. That is the autofac functions docs with my setup.

[assembly: FunctionsStartup(typeof(Sample.Startup))]

namespace Sample;
public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var configuration = builder.GetContext().Configuration;

        // https://learn.microsoft.com/en-us/azure/azure-functions/functions-dotnet-dependency-injection#use-injected-dependencies
        builder.Services.AddHttpClient();

        // Setup AutoFac and return container so we can use it later to resolve components here
        var container = SetupAutoFac(builder, configuration);

        // Configure EventBus and subscribe on integration events
        ConfigureEventBus(container);
    }

    private void ConfigureEventBus(IContainer container)
    {
        var eventBus = container.Resolve<BuildingBlocks.EventBus.Abstractions.IEventBus>();

        eventBus.Subscribe<MyIntegrationEvent, IIntegrationEventHandler<MyIntegrationEvent>>();
    }

    /// <summary>
    /// Setup AutoFac and return container so you can use it later to resolve components.
    /// </summary>
    /// <param name="builder">The IFunctionsHostBuilder</param>
    /// <param name="configuration">Configuration like appsettings, local.settings.json, user settings etc.</param>
    /// <returns>Return the AutoFac container so you can use it later to resolve components.</returns>
    private static IContainer SetupAutoFac(IFunctionsHostBuilder builder, IConfiguration configuration)
    {
        IContainer container = GetContainer(builder.Services, configuration);
        builder.Services.AddSingleton(container);

        // Important: Use AddScoped so our Autofac lifetime scope gets disposed
        // when the function finishes executing
        builder.Services.AddScoped<LifetimeScopeWrapper>();

        builder.Services.Replace(ServiceDescriptor.Singleton(typeof(IJobActivator), typeof(AutofacJobActivator)));
        builder.Services.Replace(ServiceDescriptor.Singleton(typeof(IJobActivatorEx), typeof(AutofacJobActivator)));

        return container;
    }

    /// <summary>
    /// Register everything in the AutoFac container.
    /// </summary>
    /// <param name="serviceCollection"></param>
    /// <param name="configuration">Configuration like appsettings, local.settings.json, user settings etc.</param>
    /// <returns>Return the AutoFac container so you can use it later to resolve components.</returns>
    private static IContainer GetContainer(IServiceCollection serviceCollection, IConfiguration configuration)
    {
        var containerBuilder = new ContainerBuilder();
        containerBuilder.Populate(serviceCollection);

        containerBuilder.RegisterModule<LoggerModule>();
        containerBuilder.RegisterModule(new MediatorModule());
        string connectionString = "Todo";
        containerBuilder.RegisterModule(new ApplicationModule(connectionString));

        // This is a convenient way to register all your function classes at once
        ////containerBuilder.RegisterAssemblyTypes(typeof(Startup).Assembly)
        ////    .InNamespaceOf<Function1>();

        containerBuilder.RegisterType<HttpContextAccessor>()
            .As<IHttpContextAccessor>()
            .SingleInstance();

        // PersistASB connection
        containerBuilder.Register<IServiceBusPersisterConnection>(sp =>
        {
            var serviceBusConnectionString = configuration["EventBusConnection"];

            var subscriptionClientName = configuration["SubscriptionClientName"];

            return new DefaultServiceBusPersisterConnection(serviceBusConnectionString);
        }).SingleInstance();

        // Things to test https://github.com/junalmeida/autofac-azurefunctions/issues/29#issuecomment-1413991014
        ////containerBuilder.RegisterGeneric(typeof(Logger<>))
        ////  .As(typeof(ILogger<>))
        ////  .SingleInstance();

        ////containerBuilder.Register(ctx => {
        ////    var factory = ctx.Resolve<ILoggerFactory>();
        ////          // create the logger here, (inspect ctx for T being requested) 
        ////    return logger;
        ////})
        ////  .As(typeof(ILogger<>))
        ////  .SingleInstance();

        // Setup EventBus
        containerBuilder.Register<EventBusServiceBus>(sp =>
        {
            var serviceBusPersisterConnection = sp.Resolve<IServiceBusPersisterConnection>();
            var iLifetimeScope = sp.Resolve<ILifetimeScope>();

            //  WORKAROUND THAT I THOUGHT WORKED
                        // Create a new logger that is connected to the console with a minimum loglevel from host.json based on the namespace
            var loggerFactory = LoggerFactory.Create(builder =>
            {
                var logLevel = configuration.GetValue<LogLevel>("AzureFunctionsJobHost:logging:logLevel:" + typeof(Startup).Namespace, LogLevel.Information);

                builder.AddConsole();
                builder.SetMinimumLevel(logLevel);
            });
            var logger = loggerFactory.CreateLogger<EventBusServiceBus>();

            var eventBusSubcriptionsManager = sp.Resolve<IEventBusSubscriptionsManager>();
            string subscriptionName = configuration["SubscriptionClientName"];

            return new EventBusServiceBus(serviceBusPersisterConnection, logger,
                eventBusSubcriptionsManager, iLifetimeScope, subscriptionName);
        })
            .As<IEventBus>()
            .SingleInstance();

        // Setup EventBus subscriptions
        containerBuilder.RegisterType<InMemoryEventBusSubscriptionsManager>().As<IEventBusSubscriptionsManager>().SingleInstance();

        return containerBuilder.Build();
    }
}

So containerBuilder.Register<EventBusServiceBus>(sp => crashes directly when starting the application (not even calling a function yet) because it tries to resolve ILogger<T> from the EventBusServiceBus class. That EventBusServiceBus class is resolved because of the ConfigureEventBus method and there it tries (again before a function call) to resolve var eventBus = container.Resolve<BuildingBlocks.EventBus.Abstractions.IEventBus>();.

I thought I tried ILogger as well but not sure anymore. Tried resolve ILoggerFactory, LoggerFactory etc...

Again. I try to register some general (background) handlers in the project that are not related to Function (triggers). That handler eventBus.Subscribe<MyIntegrationEvent, IIntegrationEventHandler<MyIntegrationEvent>>(); needs to start a new durable orchestrator.

junalmeida commented 1 year ago

I believe we are getting out of scope here, as this code is not related to any sample I provide here in this package. Not sure how I can help you if you don't need this package?

LockTar commented 1 year ago

Well I do need the package because I also need DI with autofac inside my functions. This works great. I was just hoping that you would see an error that I made because of your experience with this setup (autofac and functions). But I understand if you want me to close the issue because it's a bit specific. Let me know or just close it.

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.