Explorando el código detrás del MauiAppBuilder

Contenido

.NET MAUI introdujo el MauiAppBuilder que es una forma completamente nueva de «iniciar» nuestras aplicaciones multiplataformas. En lugar de utilizar App.xaml.cs tradicional todo el código de arranque va en MauiProgam.cs (posiblemente Startup.cs en el futuro) y Startup es mucho más procedimental que lo que teníamos en versiones anteriores:

...
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();
    }
}
...

Hay varias actualizaciones de C# que pueden hacer que todo esto parezca más limpio (declaraciones de nivel superior, usos implícitos, tipos lambda inferidos, etc.), pero también hay dos nuevos tipos en el juego: MauiApp y MauiAppBuilder.

En la entrada anterior describí brevemente como se compara el MauiAppBuilder con el WebApplicationBuilder de .NET Core con .NET 6. En este post vamos a ver el código detrás de MauiApp y MauiAppBuilder, para ver cómo logran una API simple, con flexibilidad y configurabilidad del Host Genérico.

Este articulo pertenece a la serie: Explorando MAUI App Builder. En esta serie habláremos sobre algunas nuevas características incluidas en el nuevo .NET MAUI.

Creación de un MauiAppBuilder

El primer paso es crear una nueva instancia de MauiAppBuilder usando método estático en MauiApp:

MauiAppBuilder builder = MauiApp.CreateBuilder();

Por detrás, esto crea una nueva instancia con las configuraciones por defecto:

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

MauiAppBuilder propoerciona una manera facil de agregar configuraciones importantes de manera predeterminada:

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

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

⚠Advertencia

MauiAppBuilder no proporciona una manera anular mediante programación estas configuraciones. Entonces, si quieres ponerte creativo tendrás que usar tu propia configuración.

En el constructor de MauiAppBuilder es donde se lleva a cabo la mayor parte de la prestidigitación para hacer que el concepto de alojamiento mínimo funcione:

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

Todavía no he mostrado el cuerpo del método, ya que hay muchas cosas sucediendo en este constructor, y muchos tipos de ayudantes, para lograrlo. Volveremos a ellos en un segundo, por ahora nos centraremos en la API pública de MauiAppBuilder.

La API pública de MauiAppBuilder

La API publica de MauiAppBuilder consiste en una cantidad de propiedades de solo lectura y un solo método, llamado Build(), que crea el MauiApp.

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

    public IHostBuilder Host => _host;

    public MauiApp Build()
}

Si está familiarizado con ASP.NET Core, muchas de estas propiedades las reconocerás al instante:

  • IServiceCollection: se utiliza para registrar servicios con el contenedor DI.
  • ConfigurationManager: se utiliza para agregar nueva configuración y recuperar valores de configuración.
  • ILoggingBuilder: se utiliza para registrar proveedores de registro adicionales.

La propiedad Host es interesante porque expone un nuevo tipo ConfigureHostBuilder. Este tipo implementa IHostBuilder y primariamente expone una forma de usar los métodos de extensiones con nuevos tipos.

ConfigureHostBuilder: una escotilla de escape para IHostBuilder

ConfigureHostBuilder se agregó como parte de la actualización «minimal hosting». Este implementa IHostBuilder:

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

Esto ayuda a pensar en ConfigureHostBuilder como un «adaptador» para los métodos de extensión existentes en lugar de un constructor de host «real». Esto se hace evidente cuando ve cómo se implementan métodos como ConfigureServices() o ConfigureAppConfiguration() en estos tipos:

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;
    }
}

El método ConfigureServices(), por ejemplo, ejecuta inmediatamente la Acción<> proporcionada utilizando la IServiceCollection inyectada desde MauiAppBuilder. Entonces, las siguientes dos llamadas son funcionalmente idénticas:

// 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>());

Este último enfoque claramente no vale la pena usarlo en la práctica normal, pero el código existente que se basa en este método (métodos de extensión, por ejemplo) aún se puede usar.

Ahora podemos volver al constructor.

El constructor de MauiAppBuilder

Por último, llegamos al constructor de MauiAppBuilder. Esto contiene mucho código, así que voy a recorrerlo pieza por pieza.

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;
}

Comenzamos con los campos y propiedades privadas.

  • _hostBuilder es una instancia del host genérico, mientras HostBuilder que es el host «interno» que respalda el MauiAppBuilder.
  • También tenemos un campo BootstrapHostBuilder que es una implementación de IHostBuilder
  • y una instancia de MauiApplicationServiceCollection la cual es una implementación de IServiceCollection  que pasaré por alto por ahora.

Los siguientes pasos en el constructor están bien documentados, afortunadamente:

...
			// 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);
...

Después de crear una instancia de BootstrapHostBuilder, la primera llamada al método es MauiHostingDefaults.ConfigureDefaults(). Este es exactamente el mismo método al que llama el host genérico cuando llama a Host.CreateDefaultBuilder() pero con algunas modificaciones.

El proximo método crea la configuración de nuestra aplicación a traves de _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);
...

Este ejecuta todas las devoluciones de llamada almacenadas que hemos acumulado hasta ahora en el orden correcto, para construir el HostBuilderContext. Luego configuramos el host con las configuraciones previas con una nueva instancia de ConfigureHostBuilder().

El HostBuilderContext se utiliza para finalmente establecer las propiedades restantes en ConfigureHostBuilder.

Finalmente, registramos la configuración por defecto de nuestra aplicación.

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

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

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

Ese es el final del constructor. En este punto, la aplicación está configurada con todos los valores predeterminados de «hosting»: configuration, logging, DI services y environment, etc.

Ahora puede agregar todos sus propios servicios, configuración adicional o iniciar sesión en 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();
    }
}
...

Una vez que haya terminado con la configuración específica de la aplicación, llame a Build() para crear una instancia de MauiApp. En la sección final de esta publicación, miramos dentro del método Build().

Construyendo un MauiApp con MauiAppBuilder.Build()

El método Build() no es muy largo, pero es un poco difícil de seguir, así que lo analizaré línea por línea:

		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) },
					});
			});

			// ...
		}

Lo primero que hacemos es agregar en memoria los datos iniciales sobre nuestra aplicación.

Tenga en cuenta que técnicamente el método ConfigureHostConfiguration no se ejecuta de inmediato. Más bien, estamos registrando una devolución de llamada que se invocará cuando llamemos a _hostBuilder.Build() en breve.

A continuación, para IServiceCollection, copiamos de la instancia de _services, en la colección de _hostBuilder. Los comentarios aquí son bastante explicativos; en este caso, la colección de servicios de _hostBuilder no está completamente vacía (solo en su mayor parte vacía), pero agregamos todo desde Servicios y luego «restablecemos» Servicios a la instancia de _hostBuilder.

		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));
					}
				}
			});
		}

En la siguiente línea, ejecutamos las devoluciones de llamada que hemos recopilado en la propiedad ConfigureHostBuilder, como mencioné anteriormente.

		public MauiApp Build()
		{
			// ...

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

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

			// ...
		}

Finalmente, llamamos a _hostBuilder.Build() para construir la instancia de Host y pasarla a una nueva instancia de MauiApp. La llamada a _hostBuilder.Build() es donde se invocan todas las devoluciones de llamada registradas.

Finalmente, tenemos un poco de limpieza. Para mantener todo coherente, IServiceCollection en MauiAppBuilder está marcado como de solo lectura, por lo que intentar agregar servicios después de llamar a MauiAppBuilder arrojará una InvalidOperationException. Por último, se devuelve la aplicación Maui.

		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;
		}

Resumen

En esta publicación, echamos un vistazo a parte del código detrás de la nueva API de alojamiento mínimo de MauiAppBuilder. Mostré cómo el tipo ConfigureHostBuilder actúa como adaptador para el tipo de host genérico.

Hubo mucho código confuso solo para crear una instancia de MauiAppBuilder, pero terminamos la publicación llamando a Build() para crear una aplicación maui.

Si todavía no me sigues en las redes, lo puedes hacer en Twitter o Linkedin. Sera genial verte por allá.

¿Qué opinas de este contenido?
 
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.
Suscribirte
Notificar de
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x

Buscar en el sitio