.Net 5 : Que se cache-t’il derrière la méthode Host.CreateDefaultBuilder(args)
Lorsqu’un projet .Net 5 est créé à partir du template Web Application Asp.Net, ce dernier contient un fichier Program.cs. Le contenu de ce dernier est présenté ci-dessous.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Cette classe contient deux méthodes : Main et CreateHostBuilder.
La méthode Main, comme dans la plupart des programmes informatiques, est le point d’entrée de notre application. Si nous démarrons notre application en mode pas à pas (F10 ou F11), notre premier arrêt sera sur l’accolade ouvrante de cette méthode.

La seconde méthode CreateHostBuilder retourne un objet de type IHostBuilder. Avant de rentrer dans les détails, définissons ce qu’est qu’un host.
Un Host est un objet héritant de IHost qui va gérer :
- Le démarrage de notre application et son cycle de vie
- L’injection de dépendances
- Le logging
- La configuration de notre application
- …
Cet objet sera généré par la méthode Build() de notre instance d’IHostBuilder. Nous pouvons voir dans le code que nous récupérons cette instance via la ligne suivante :
Host.CreateDefaultBuilder(args)
Host.cs
La classe Host est une classe statique disponible dans la librairie Microsoft.Extensions.Hosting, et qui contient deux méthodes : CreateDefaultBuilder() et CreateDefaultBuilder(args). Pour bien comprendre la magie qui se passe derrière cette méthode, profitons que le code source de cette librairie est disponible sur GitHub et analysons le code source de cette classe statique.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Provides convenience methods for creating instances of <see cref="IHostBuilder"/> with pre-configured defaults.
/// </summary>
public static class Host
{
/// <summary>
/// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults.
/// </summary>
/// <remarks>
/// The following defaults are applied to the returned <see cref="HostBuilder"/>:
/// <list type="bullet">
/// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item>
/// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item>
/// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item>
/// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item>
/// </list>
/// </remarks>
/// <returns>The initialized <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder CreateDefaultBuilder() =>
CreateDefaultBuilder(args: null);
/// <summary>
/// Initializes a new instance of the <see cref="HostBuilder"/> class with pre-configured defaults.
/// </summary>
/// <remarks>
/// The following defaults are applied to the returned <see cref="HostBuilder"/>:
/// <list type="bullet">
/// <item><description>set the <see cref="IHostEnvironment.ContentRootPath"/> to the result of <see cref="Directory.GetCurrentDirectory()"/></description></item>
/// <item><description>load host <see cref="IConfiguration"/> from "DOTNET_" prefixed environment variables</description></item>
/// <item><description>load host <see cref="IConfiguration"/> from supplied command line args</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from 'appsettings.json' and 'appsettings.[<see cref="IHostEnvironment.EnvironmentName"/>].json'</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from User Secrets when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development' using the entry assembly</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from environment variables</description></item>
/// <item><description>load app <see cref="IConfiguration"/> from supplied command line args</description></item>
/// <item><description>configure the <see cref="ILoggerFactory"/> to log to the console, debug, and event source output</description></item>
/// <item><description>enables scope validation on the dependency injection container when <see cref="IHostEnvironment.EnvironmentName"/> is 'Development'</description></item>
/// </list>
/// </remarks>
/// <param name="args">The command line args.</param>
/// <returns>The initialized <see cref="IHostBuilder"/>.</returns>
public static IHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new HostBuilder();
builder.UseContentRoot(Directory.GetCurrentDirectory());
builder.ConfigureHostConfiguration(config =>
{
config.AddEnvironmentVariables(prefix: "DOTNET_");
if (args != null)
{
config.AddCommandLine(args);
}
});
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
IHostEnvironment env = hostingContext.HostingEnvironment;
bool reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
.ConfigureLogging((hostingContext, logging) =>
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// IMPORTANT: This needs to be added *before* configuration is loaded, this lets
// the defaults be overridden by the configuration.
if (isWindows)
{
// Default the EventLogLoggerProvider to warning or above
logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
}
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
if (isWindows)
{
// Add the EventLogLoggerProvider on windows machines
logging.AddEventLog();
}
logging.Configure(options =>
{
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
| ActivityTrackingOptions.TraceId
| ActivityTrackingOptions.ParentId;
});
})
.UseDefaultServiceProvider((context, options) =>
{
bool isDevelopment = context.HostingEnvironment.IsDevelopment();
options.ValidateScopes = isDevelopment;
options.ValidateOnBuild = isDevelopment;
});
return builder;
}
}
}
La méthode CreateDefaultBuilder() se contente simplement d’appeler la méthode CreateDefaultBuilder(string[] args) en lui passant la valeur null.
var builder = new HostBuilder();
La méthode CreateDefaultBuilder(string[] args) commence par l’instanciation d’un objet de type HostBuilder (Qui hérite de IHostBuilder). Nous pourrions directement appeler la méthode Build().Run() sur cette instance pour démarrer notre hôte Asp.Net, mais ce dernier n’aura aucune configuration.
Configuration de l’Host
builder.UseContentRoot(Directory.GetCurrentDirectory());
Cette méthode permet de définir le répertoire dans lequel le Host sera exécuté. Notre Host sera donc, par défaut, exécuté dans le répertoire depuis lequelle la commande CLI dotnet sera lancée.
builder.ConfigureHostConfiguration(config =>
{
config.AddEnvironmentVariables(prefix: "DOTNET_");
if (args != null)
{
config.AddCommandLine(args);
}
});
Cette partie concerne, comme son nom l’indique, la configuration du Host. Il peut avoir quelques confusions concernant la méthode ConfigureHostConfiguration et la méthode ConfigureAppConfiguration puisque ces dernières reçoivent un delegate avec un paramètre de type IConfigurationBuilder. Ce que nous pouvons retenir, c’est que la configuration d’un Host concerne uniquement les points suivant s:
- Le nom de l’app
- L’environnement
- Le répertoire racine
- Le Kestrel
La méthode UseContentRoot() se contente, d’ailleurs d’ajouter une configuration à notre Host
Toutes les autres configurations concernent la configuration de l’app. L’étape suivante est donc la configuration de l’app.
Configuration de l’app
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
IHostEnvironment env = hostingContext.HostingEnvironment;
bool reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
La variable hostingContext.HostingEnvironment contient l’environnement dans lequel sera exécuté notre application (Development, staging ou production).
Chargement des fichiers de configurations
bool reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: reloadOnChange)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: reloadOnChange);
La configuration de l’app commence donc par charger nos deux fichiers appsettings.json. Cette action est réalisée grâce à la méthode AddJsonFile(). Cette méthode prend 3 paramètres :
- Path (string) : Le chemin d’accès aux fichiers json. Le chemin est relatif, et se base sur le répertoire que nous passons en paramètre à la méthode UseContentRoot().
- Optional (bool) : Spécifie si le fichier est requis (false) ou est optionnel (true). Si le paramètre possède la valeur false et que le fichier est introuvable, une exception de type FileNotFoundException est levé et notre app ne se lancera pas.
- ReloadOnChange (bool) : Si la valeur est à true, les valeurs seront automatiqument rechargée pendant que notre app tourne. si le contenu du fichier change.
bool reloadOnChange = hostingContext.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true);
Cette ligne est une nouveauté du .Net 5. En .Net Core 3.x, la valeur de reloadOnChange était systématiquement à true. Ici, il est possible de mettre la valeur à false en ajoutant la configuration suivante « hostBuilder:reloadConfigOnChange » à false.
Ajout des User Secrets
if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
}
Les paramètres de configuration contenant des données sensibles ne doivent pas remonter dans notre repository. En .Net Core, le mécanisme des User Secrets nous permet d’avoir un fichier secrets.json qui permettra d’écraser les données présentes dans le fichier appSettings.json. Si vous avez enregistré des Users Secrets dans votre environnement, cette partie s’occupera de les charger.
Logging
.ConfigureLogging((hostingContext, logging) =>
{
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// IMPORTANT: This needs to be added *before* configuration is loaded, this lets
// the defaults be overridden by the configuration.
if (isWindows)
{
// Default the EventLogLoggerProvider to warning or above
logging.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Warning);
}
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
logging.AddEventSourceLogger();
if (isWindows)
{
// Add the EventLogLoggerProvider on windows machines
logging.AddEventLog();
}
logging.Configure(options =>
{
options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId
| ActivityTrackingOptions.TraceId
| ActivityTrackingOptions.ParentId;
});
})
Cette étape consiste à la configuration des logs générés par l’application. Nous pouvons voir que, si l’app tourne sous Windows, les logs dont le niveau est supérieur à warning seront écrits dans l’eventLog de Windows, mais seront également écris dans la console (AddConsole) et également dans la fenêtre Output présente dans Visual Studio (AddDebug). AddEventSourceLogger ajoute l’Event Tracing.
Injection de dépendance
.UseDefaultServiceProvider((context, options) =>
{
bool isDevelopment = context.HostingEnvironment.IsDevelopment();
options.ValidateScopes = isDevelopment;
options.ValidateOnBuild = isDevelopment;
});
La dernière étape de notre méthode CreateDefaultBuilder est l’ajout de l’injection de dépendance dans notre app. La méthode UseDefaultServiceProvider permet d’ajouter le container IOC natif à .Net Core. Si l’option ValidateScopes est à true, elle lèvera une exception si vous tentez de résoudre une dépendance enregistrée en tant que Scoped dans une méthode qui n’a pas de scope (Par exemple, si vous tentez de résoudre votre dépendance dans la méthode Configure de votre classe Startup.cs).
La méthode ValidateOnBuild est une nouvelle fonctionnalité du .Net Core 3 qui, si sa valeur est à true, lancera une exception si une de vos dépendances a besoin d’une dépendance que nous aurions oublié d’enregistrer. Je vous invite à lire mes articles pour en savoir plus sur l’injection de dépendances.
Conclusion
La méthode Host.CreateDefaultBuilder(args) n’a à présent plus de secret pour vous. Dans un prochain article, je vous montrerai comment créer une app en .Net Core sans utiliser cette méthode. À très bientôt 🙂