How to add a custom ModelMetadataDetailsProvider that has a dependency that needs to be resolved by Autofac?

Viewed 1026

I'm trying to add a custom ModelMetadataDetailsProvider, but the provider implementation has dependencies that need to be resolved by the Service Provider (Autofac). If I Add the ModelMetadataDetailsProvider in ConfigureServices, I have to instantiate and manually provide all of the dependencies, some of which are singleton and are AutoActivated, so that won't work... Is it possible to add a ModelMetadataDetailsProvider outside of ConfigureServices?

It doesn't appear that this can be configured using DI, so the only thing I can think of is to use the Service Locator Anti Pattern to provide the dependencies when they are needed instead of in the constructor. Is there a more acceptable way to accomplish this?

public IServiceProvider ConfigureServices(IServiceCollection services)
{
  services.AddMvc()
          .AddMvcOptions(options => {
             options.ModelMetadataDetailsProviders.Add(new MyProvider(???))
          })
          .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
          .AddControllersAsServices();

  services.AddAutofac();

  ApplicationContainer = BuildContainer(services);

  return new AutofacServiceProvider(ApplicationContainer);
}

public IContainer BuildContainer(IServiceCollection services)
{
  var builder = new ContainerBuilder();
  builder.Populate(services);

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

  builder.RegisterType<DataAccess>()
      .As<IDataAccess>()
      .WithParameter("connectionString", Configuration.GetConnectionString("DatabaseContext"))
      .InstancePerLifetimeScope();

   ....

   builder.RegisterType<D1>()
      .As<ID1>();

   builder.RegisterType<D2>()
      .As<ID2>();

   builder.RegisterType<D3>()
      .As<ID3>();

   builder.RegisterType<MyProvider>()
      .As<IMyProvider>();
 }



public interface IMyProvider : IDisplayMetadataProvider
{
  ...
}

public class MyProvider : IMyProvider
{
    public MyProvider (ID1 d1, ID2 d2, ID3 d3)
    {
      ...
    }

    public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
    {
      ...
    }
}
1 Answers

You can achieve this by creating a class that implements the IConfigureOptions<MvcOptions> interface:

public class AddCustomModelMetadataDetailsProvider : IConfigureOptions<MvcOptions>
{
    private readonly MyCustomModelMetadataDetailsProvider _provider;

    public AddCustomModelMetadataDetailsProvider(MyCustomModelMetadataDetailsProvider provider)
    {
        _provider = provider;
    }

    public void Configure(MvcOptions options)
    {
        options.ModelMetadataDetailsProviders.Add(_provider);
    }
}

and register it as such in the Configure method:

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

The benefit of this approach, as you can see, is that you can use regular constructor injection in the AddCustomModelMetadataDetailsProvider class to get the instance of the service you're interested in.

ASP.NET automatically calls the Configure methods of all the IConfigureOptions<MvcOptions> services registered in the container.


Because creating these classes can be time-consuming, ASP.NET Core 2.2 introduced new overloads which allow you to do the following:

services
    .AddOptions<MvcOptions>()
    .Configure<MyCustomModelMetadataDetailsProvider>((options, customMetadataDetailsProvider) =>
    {
         options.ModelMetadataDetailsProviders.Add(customMetadataDetailsProvider);
    });

In that case, customMetadataDetailsProvider would be resolved from the container.

You can include up to 5 services to configure your options. See this official documentation page.

Related