5

A few years ago I created a suite of nuget packages that I use for CQRS which use AutoFac modules to wire up the internals. I'd like to use that in my .NET MAUI development so I've updated them to .NET 6.0 and they link in with my MAUI project nicely, but I'm uncertain what's missing from my registrations. My framework's AutoFac Module registers an IDateTimeService but when I add that to a registered class' constructor it can't be resolved.

So, following the AutoFac guide for .NET Core I've added the Populate call and then Load the AutoFac module.

using Autofac;
using Autofac.Extensions.DependencyInjection;
using Pages;
using Perigee.Framework.Services;
using Services;
using ViewModels;


public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });
        
        builder.Services.AddSingleton<IAppNavigationService, AppNavigationService>();
        
        builder.Services.AddSingleton<AppShellViewModel>();
        builder.Services.AddTransient<MainPageViewModel>();

        builder.Services.AddSingleton<AppShell>();
        builder.Services.AddSingleton<MainPage>();
        

        // Hook in AutoFac for the PerigeeFramework services
        var autofacBuilder = new ContainerBuilder();
        autofacBuilder.Populate(builder.Services);

        autofacBuilder.RegisterModule(new ServicesModule());
        autofacBuilder.Build(); // Invokes the Load method on the registered Modules.


        return builder.Build();
    }
}

The AutoFac Module starts like this:

    public class ServicesModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            builder.Register(c =>
            {
                var config = c.IsRegistered<IDateTimeConfig>() ? c.Resolve<IDateTimeConfig>() : null;
                return new DateTimeService(config);
            }).As<IDateTimeService>().InstancePerLifetimeScope();

and this is the definition of AppShellViewModel

    public AppShellViewModel(IDateTimeService dateTimeService)

which is injected into the AppShell:

public partial class AppShell : Shell
{
    public AppShell(AppShellViewModel viewModel)
    {
        InitializeComponent();

        BindingContext = viewModel;

At run time the IDateTimeService doesn't resolve. I've also tried just registering with AutoFac without a module and it won't resolve:

        // Hook in AutoFac for the PerigeeFramework services
        var autofacBuilder = new ContainerBuilder();
        autofacBuilder.Populate(builder.Services);

        autofacBuilder.RegisterType<DateTimeService>().As<IDateTimeService>().SingleInstance();
        
        var cont = autofacBuilder.Build();


        return builder.Build();
    }

enter image description here

The key reason I needed something other than .NET DI was because the architecture leverages decorators, which SimpleInjector and AutfoFac provide out of the box so I chose AutoFac. In either case I need to use this "crosswire" approach to use AutoFac and .NET DI as MAUI is using the built in one. Does anyone know what step I'm missing that is preventing the registrations from an AutoFac module from appearing in the IServiceCollection, or can I completely replace the .NET DI with AutoFac on the MauiApp?

EDIT: I've put together a trimmed down version of my app. I figured maybe I need to pass a new AutoFacServiceProvider through to the App and the ISomeService does resolve when registered with AutoFac enter image description here

But the call to MainPage = serviceProvider.GetService<AppShell>() fails to resolve if I try to inject ISomeService into another registered class. If the service is registered with the standard DI it will work.

Anyone know how to propogate the AutoFac Service Provider as the one Maui will use? The project is here

Stephen York
  • 1,247
  • 1
  • 13
  • 42

2 Answers2

3

The MauiAppBuilder (called from the MauiProgram) has a method called ConfigureContainer, it takes an IServiceProvider factory that Autofac provides as AutofacServiceProviderFactory and optionally it can take an Action<ContainerBuilder> delegate, where you can define your configuration.

In your case that could look like this:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .ConfigureContainer(new AutofacServiceProviderFactory(), autofacBuilder => {
                // Registrations
                // Don't call the autofacBuilder.Build() here - it is called behind the scenes
            });

        return builder.Build();
    }
}
BetaDeltic
  • 31
  • 4
  • 1
    Thanks for sharing the answer. It's helpful. But, I've no idea on how to access the container instance from anywhere in the project. Please help me with this – Akshay Kumar Feb 20 '23 at 11:59
  • 3
    @AkshayKumar - I don't know if there's a way to access the container directly. But if just need to get any registered component - you can put IServiceProvider as a dependency in any constructor of the project. (it will get resolved) With it you can call GetService() to access any registered components. Example: YourComponentConstructor(IServiceProvider provider) { var yourComponent = provider.GetService() } – BetaDeltic Feb 21 '23 at 12:56
1

It looks like you can hook into the AutofacServiceProviderFactory to get access to the Autofac.ContainerBuilder. Its constructor takes an Action<ContainerBuilder> argument. Something like:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .ConfigureContainer(
                new AutofacServiceProviderFactory(containerBuilder =>
                {

                }));

        return builder.Build();
    }
}

It may make more sense to create your own IServiceProviderFactory<Autofac.ContainerBuilder> though, and pass it instead of the AutofacServiceProviderFactory. That way you can do what AutofacServiceProviderFactory does but you'll also be taking over responsibility for creating the container, and at that point you'll have direct access to it. Something like:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .ConfigureContainer(
                new MyServiceProviderFactory());

        return builder.Build();
    }
}
...
public class MyServiceProviderFactory : IServiceProviderFactory<Autofac.ContainerBuilder>
{
    public ContainerBuilder CreateBuilder(IServiceCollection services)
    {
        // this is what AutofacServiceProviderFactory essentially does
        var builder = new ContainerBuilder();

        builder.Populate(services);

        return builder;
    }

    public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
    {   // this is what AutofacServiceProviderFactory essentially does
        if (containerBuilder == null)
        {
            throw new ArgumentNullException(nameof(containerBuilder));
        }

        var container = containerBuilder.Build();

        // put your code to use the container here

        return new AutofacServiceProvider(container);
    }
}
bcr
  • 1,983
  • 27
  • 30