Exploring the code behind MauiAppBuilder

Contenido

.NET MAUI introduced the MauiAppBuilder which is a completely new way to “boot” our cross-platform applications. Instead of using  App.xaml.cs  traditional all the boot code goes in MauiProgam.cs (possibly Startup.cs in the future) and Startup is much more procedural than what we had in previous versions:

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

        return builder.Build();
    }
}
...

There are several C# updates that can make all of this seem cleaner (top-level statements, implicit uses, inferred lambda types, etc.), but there are also two new types in the game: MauiApp and MauiAppBuilder.

In the previous post, I briefly described how MauiAppBuilder compares to the .NET Core WebApplicationBuilder to .NET 6. In this post we will look at the code behind MauiApp and MauiAppBuilder, to see how they achieve a simple API, with flexibility and configurability of the Generic Host.

This article belongs to the series: Exploring MAUI App Builder. In this series, we will talk about some new features included in the new .NET MAUI.

Creating a MauiAppBuilder

The first step is to create a new mauiAppbuilder instance using static method in MauiApp:

MauiAppBuilder builder = MauiApp.CreateBuilder();

From behind, this creates a new instance with the default configuration:

public static MauiAppBuilder CreateBuilder(bool useDefaults = true) => new(useDefaults);

MauiAppBuilder offers an easy way to add important configuration by default:

if (useDefaults)
{
		// Register required services
		this.ConfigureMauiHandlers(configureDelegate: null);

		this.ConfigureFonts();
		this.ConfigureImageSources();
		this.ConfigureAnimations();
		this.ConfigureCrossPlatformLifecycleEvents();
}

⚠Warning

MauiAppBuilder does not provide a programmatic way to override these configurations. So if you want to get creative you’ll have to use your own configurations.

The MauiAppBuilder builder is where most of the sleight of hand takes place to make the concept of minimal hosting work:

public sealed class MauiAppBuilder
{
    internal MauiAppBuilder(bool useDefaults = true)
    {
        // .. shown below
    }
...

I have not yet shown the body of the method, as there are many things going on in this builder, and many types of helpers, to achieve it. We’ll come back to them in a second, for now, we’ll focus on the MauiAppBuilder public API.

The MauiAppBuilder Public API

The MauiAppBuilder public API consists of a number of read-only properties and a single method, called Build(), that creates the MauiApp.

public sealed class MauiAppBuilder
{
    public IServiceCollection Services { get; }
    public ConfigurationManager Configuration { get; }
    public ILoggingBuilder Logging => _logging;

    public IHostBuilder Host => _host;

    public MauiApp Build()
}

If you’re familiar with ASP.NET Core, many of these properties will be instantly recognized:

  • IServiceCollection: Used to register services with the DI container.
  • ConfigurationManager– Used to add new configurations and retrieve configuration settings.
  • ILoggingBuilder– Used to register additional logging providers.

The Host property is interesting because it exposes a new ConfigureHostBuilder type. This type implements IHostBuilder and primarily exposes a way to use extension methods with new types.

ConfigureHostBuilder: An Escape Hatch for IHostBuilder

ConfigureHostBuilder was added as part of the “minimal hosting” update. This IHostBuilderimplementation:

public sealed class ConfigureHostBuilder : IHostBuilder
{
    // ...
}

This helps to think of ConfigureHostBuilder as an “adapter” for existing extension methods rather than a “real” host builder. This becomes apparent when you see how methods such as  ConfigureServices()  or  ConfigureAppConfiguration()  are implemented in these types:

public sealed class ConfigureHostBuilder : IHostBuilder, ISupportsConfigureWebHost
{
    private readonly ConfigurationManager _configuration;
    private readonly IServiceCollection _services;
    private readonly HostBuilderContext _context;

    internal ConfigureHostBuilder(HostBuilderContext context, ConfigurationManager configuration, IServiceCollection services)
    {
        _configuration = configuration;
        _services = services;
        _context = context;
    }

    public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
    {
        // Run these immediately so that they are observable by the imperative code
        configureDelegate(_context, _configuration);
        return this;
    }

    public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
    {
        // Run these immediately so that they are observable by the imperative code
        configureDelegate(_context, _services);
        return this;
    }
}

The ConfigureServices() method, for example, immediately executes the Action<> provided using the IServiceCollection injected from MauiAppBuilder. So, the following two calls are functionally identical:

// directly registers the MyImplementation type with the IServiceContainer
builder.Services.AddSingleton<MyImplementation>();

// uses the "legacy" ConfigureServices method
builder.Host.ConfigureServices((ctx, services) => services.AddSingleton<MyImplementation>());

The latter approach is clearly not worth using in normal practice, but the existing code that is based on this method (extension methods, for example) can still be used.

Now we can go back to the builder.

The MauiAppBuilder constructor

Finally, we come to the MauiAppBuilder builder. This contains a lot of code, so I’m going to go through it piece by piece.

public sealed class MauiAppBuilder
{
		private readonly HostBuilder _hostBuilder = new();
		private readonly BootstrapHostBuilder _bootstrapHostBuilder;
		private readonly MauiApplicationServiceCollection _services = new();
		private readonly LoggingBuilder _logging;
		private readonly ConfigureHostBuilder _host;
		private MauiApp? _builtApplication;

		internal MauiAppBuilder(bool useDefaults = true)
		{
			Services = _services;

			// ...
		}

    public IServiceCollection Services { get; }
    public ConfigurationManager Configuration { get; }
    public ILoggingBuilder Logging => _logging;
    public IHostBuilder Host => _host;
}

We start with the fields and private properties.

  • _hostBuilder is an instance of the generic host, while HostBuilder is the “internal” host that supports the MauiAppBuilder.
  • We also have a BootstrapHostBuilder field which is an implementation of IHostBuilder
  • and an instance of MauiApplicationServiceCollection which is an implementation of IServiceCollection that I will overlook for now.

The following steps in the builder are well documented, fortunately:

...
			// Run methods to configure both generic and web host defaults early to populate config from appsettings.json
			// environment variables (both DOTNET_ and ASPNETCORE_ prefixed) and other possible default sources to prepopulate
			// the correct defaults.
			_bootstrapHostBuilder = new BootstrapHostBuilder(Services, _hostBuilder.Properties);

			MauiHostingDefaults.ConfigureDefaults(_bootstrapHostBuilder);
...

After you create an instance of BootstrapHostBuilder, the first method call is MauiHostingDefaults.ConfigureDefaults(). This is exactly the same method that the generic host calls when it calls  Host.CreateDefaultBuilder()  but with some modifications.

The next method creates the configuration of our application through  _bootstrapHostBuilder.RunDefaultCallbacks().

...
			Configuration = new();

			// This is the application configuration
			var hostContext = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder);

			_logging = new LoggingBuilder(Services);
			_host = new ConfigureHostBuilder(hostContext, Configuration, Services);
...

This executes all the stored callbacks that we have accumulated so far in the correct order, to build the HostBuilderContext. We then configure the host with the previous configurations with a new instance of  ConfigureHostBuilder().

The HostBuilderContext is used to finally set the remaining properties in ConfigureHostBuilder.

Finally, we record the default configuration of our application.

...
			Services.AddSingleton<IConfiguration>(_ => Configuration);

			if (useDefaults)
			{
				// Register required services
				this.ConfigureMauiHandlers(configureDelegate: null);

				this.ConfigureFonts();
				this.ConfigureImageSources();
				this.ConfigureAnimations();
				this.ConfigureCrossPlatformLifecycleEvents();
			}
...

That’s the end of the builder. At this point, the application is configured with all the default values of “hosting”: configuration, logging, DI services, and environment, etc.

You can now add all your own services, additional configuration, or log in to MauiAppBuilder:

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

        return builder.Build();
    }
}
...

Once you’re done with the app-specific configuration, call Build() to create a MauiApp instance. In the final section of this post, we look inside the  Build() method.

Building a MauiApp with  MauiAppBuilder.Build()

The Build() method isn’t very long, but it’s a bit difficult to follow, so I’ll analyze it line by line:

		public MauiApp Build()
		{
      // Copy the configuration sources into the final IConfigurationBuilder
			_hostBuilder.ConfigureHostConfiguration(builder =>
			{
				builder.AddInMemoryCollection(
					new Dictionary<string, string> {
						{ HostDefaults.ApplicationKey, BootstrapHostBuilder.GetDefaultApplicationName() },
						{ HostDefaults.ContentRootKey,  Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) },
					});
			});

			// ...
		}

The first thing we do is add in memory the initial data about our application.

Note that technically the ConfigureHostConfiguration method does not run immediately. Rather, we are recording a callback that will be invoked when we call _hostBuilder.Build() shortly.

Next, for IServiceCollection, we copy from the _servicesinstance, into the _hostBuildercollection. The comments here are quite explanatory; in this case, the collection of _hostBuilder services is not completely empty (only mostly empty), but we add everything from Services and then “reset” Services to the _hostBuilder instance.

		public MauiApp Build()
		{
			// ...

			// Chain the configuration sources into the final IConfigurationBuilder
			var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration);

			_hostBuilder.ConfigureAppConfiguration(builder =>
			{
				builder.Add(chainedConfigSource);

				foreach (var kvp in ((IConfigurationBuilder)Configuration).Properties)
				{
					builder.Properties[kvp.Key] = kvp.Value;
				}
			});

			// This needs to go here to avoid adding the IHostedService that boots the server twice (the GenericWebHostService).
			// Copy the services that were added via WebApplicationBuilder.Services into the final IServiceCollection
			_hostBuilder.ConfigureServices((context, services) =>
			{
				// We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults
				// at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen
				// until now, so we cannot clear these services even though some are redundant because
				// we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
				foreach (var s in _services)
				{
					services.Add(s);
				}

				// Drop the reference to the existing collection and set the inner collection
				// to the new one. This allows code that has references to the service collection to still function.
				_services.InnerCollection = services;

				var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers;

				if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider))
				{
					// Something removed the _hostBuilder's TrackingChainedConfigurationSource pointing back to the ConfigurationManager.
					// Replicate the effect by clearing the ConfingurationManager sources.
					((IConfigurationBuilder)Configuration).Sources.Clear();
				}

				// Make builder.Configuration match the final configuration. To do that, we add the additional
				// providers in the inner _hostBuilders's Configuration to the ConfigurationManager.
				foreach (var provider in hostBuilderProviders)
				{
					if (!ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
					{
						((IConfigurationBuilder)Configuration).Add(new ConfigurationProviderSource(provider));
					}
				}
			});
		}

In the next line, we run the callbacks that we have collected in the  ConfigureHostBuilder property, as I mentioned earlier.

		public MauiApp Build()
		{
			// ...

			// Run the other callbacks on the final host builder
			_host.RunDeferredCallbacks(_hostBuilder);

			_builtApplication = new MauiApp(_hostBuilder.Build());

			// ...
		}

Finally, we call _hostBuilder.Build() to build the Host instance and move it to a new MauiApp instance. The call to _hostBuilder.Build() is where all registered callbacks are invoked.

Finally, we have some cleaning. To keep everything consistent,  IServiceCollection in  MauiAppBuilder is marked as read-only, so trying to add services after calling MauiAppBuilder will throw an  InvalidOperationException. Finally, the Maui application is returned.

		public MauiApp Build()
		{
			// ...

			// Mark the service collection as read-only to prevent future modifications
			_services.IsReadOnly = true;

			// Resolve both the _hostBuilder's Configuration and builder.Configuration to mark both as resolved within the
			// service provider ensuring both will be properly disposed with the provider.
			_ = _builtApplication.Services.GetService<IEnumerable<IConfiguration>>();

			var initServices = _builtApplication.Services.GetServices<IMauiInitializeService>();
			if (initServices != null)
			{
				foreach (var instance in initServices)
				{
					instance.Initialize(_builtApplication.Services);
				}
			}

			return _builtApplication;
		}

Summary

In this post, we take a look at some of the code behind MauiAppBuilder’s new minimum hosting API. I showed how the ConfigureHostBuilder type acts as an adapter for the generic host type.

There was a lot of confusing code just to create an instance of  MauiAppBuilder, but we ended the post by calling Build() to create a Maui app.

If you still don’t follow me on social media, you can do it on  Twitter or  Linkedin. It will be great to see you over there.

What do you think of this content?
 
Luis Matos

Luis Matos

I help professionals and companies to create value solutions. I am a Systems Engineer, blockchain executive, and international mobile application speaker. Founder of the Malla Consulting Agency and several international technology communities.
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x

Search in the site