4

I have the following code in my Startup class, which uses a custom instance of IModelBinderProvider. Most input formatters require a ILogger instance, so I need to have an ILoggerFactory, and for that I want to use the configured one, which logs to specified destinations at specified verbosity. I'm grabbing the ILoggerFactory from a newly built IServiceProvider:

    public void ConfigureServices(IServiceCollection services)
    {
        ...
    
        services.AddMvcCore(
            options =>
            {
                ...
            
                var serviceProvider = services.BuildServiceProvider();
                var loggerFactory = serviceProvider.GetService<ILoggerFactory>();

                options.ModelBinderProviders.Clear();
                options.ModelBinderProviders.Add(
                    new MyCustomBinderProvider(options.InputFormatters, loggerFactory)
                );
                
                ...
            }
        );
        
        ...
    }

The problem is that I'm getting the following warning:

Startup.cs(62, 43): [ASP0000] Calling 'BuildServiceProvider' from application code results in an
                    additional copy of singleton services being created. Consider alternatives such
                    as dependency injecting services as parameters to 'Configure'.

I took a look on this question: Resolving instances with ASP.NET Core DI from within ConfigureServices. However, my code is a lambda of AddMvcCore, which is configuring the options object. In other words, by the time the Configure is called, the MVC options are already defined.

Is there a way of doing the right thing here, i.e. prevent an extraneous instance of IServiceProvider?

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
fernacolo
  • 7,012
  • 5
  • 40
  • 61
  • Thanks @Nkosi. My custom binding provider is inspired by `BodyModelBinderProvider`, which takes an `ILoggerFactory` as input. In fact, there is already an instance of the `BodyModelBinderProvider`, the reason for which I have to call `Clear`. If ASP.NET Core are able to resolve the logger factory early, I think I should be able to do the same... – fernacolo Mar 09 '21 at 02:42

2 Answers2

6

There are several ways to inject services at the time configuring an options. The first way is by using an OptionsBuilder<TOptions> which can be obtained by using the extension method IServiceCollection.AddOptions, like this:

services.AddOptions<MvcOptions>()
         //first arg is always of TOptions, 
         //injectable dependencies start from the second arg
        .Configure((MvcOptions o, ILoggerFactory loggerFactory) => {
                       o.ModelBinderProviders.Insert(0,
                          new MyCustomBinderProvider(o.InputFormatters, loggerFactory)
                       );
                   });

The second way is by implementing an IConfigureOptions<TOptions> (or IPostConfigureOptions<TOptions>), like this:

public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
{
    readonly ILoggerFactory _loggerFactory;
    public ConfigureMvcOptions(ILoggerFactory loggerFactory)
    {
        _loggerFactory = loggerFactory;
    }
    public void Configure(MvcOptions options)
    {
        options.ModelBinderProviders.Insert(0,
                  new MyCustomBinderProvider(options.InputFormatters, _loggerFactory)
               );
    }
}

//then configure it like this:
services.ConfigureOptions<ConfigureMvcOptions>();

The first way is convenient because you don't have to create a separate class for implementation of IConfigureOptions<Options> but it has a limited number of injectable dependencies. The second way allows you to inject as many as you want and suitable for a large amount of configuring code.

King King
  • 61,710
  • 16
  • 105
  • 130
1

You don't need to build the ServiceProvider to resolve the ILoggerFactory to pass it as a parameter.

Inside of your MyCustomBinderProvider you have access to resolve the ILoggerFactory, I think you should have something like this

public class MyCustomBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(CustomDto))
        {
            var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
            return new MyCustomBinder(logger);
        }

        return null;
    }
}
Zinov
  • 3,817
  • 5
  • 36
  • 70