This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
mfgames-toolbuilder-cil/src/MfGames.ToolBuilder/ToolBoxBuilder.cs
2022-04-02 19:14:25 -05:00

187 lines
6.8 KiB
C#

using System;
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.Parsing;
using System.IO;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using MfGames.ToolBuilder.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
namespace MfGames.ToolBuilder
{
/// <summary>
/// A builder pattern for creating a tool box, a nested collection of tools.
/// This wraps much of the hosting and command-line parsing infrastructure
/// with some opinionated decisions and reduces the amount of boilerplate
/// needed to create tools.
/// </summary>
public class ToolBoxBuilder
{
private readonly string[] arguments;
private readonly ConfigToolGlobal config;
private readonly IHostBuilder hostBuilder;
private readonly LoggingToolGlobal logging;
public ToolBoxBuilder(string configName, string[] arguments)
{
// Create our various services.
this.arguments = arguments;
this.config = new ConfigToolGlobal(configName);
this.logging = new LoggingToolGlobal();
// Set up logging first so we can report the loading process. This
// sets up the Serilog.Log.Logger which means we can use that for
// everything beyond this point.
this.logging.Configure(arguments);
// Start up the basic configuration.
//
// Normally, we would use Host.CreateDefaultBuilder(args) to create
// this, but when run as a stand-alone application in someone's
// $HOME on Linux, this causes an inotify watcher to be registered
// on the entire directory tree (which takes a really long time).
//
// We also don't need most of the default features.
var serviceProviderFactory = new AutofacServiceProviderFactory();
this.hostBuilder = new HostBuilder()
.UseDefaultServiceProvider(this.ConfigureDefaultServiceProvider)
.UseConsoleLifetime()
.ConfigureAppConfiguration(this.ConfigureAppConfiguration)
.UseSerilog()
.UseServiceProviderFactory(serviceProviderFactory)
.ConfigureServices(this.ConfigureServices)
.ConfigureContainer<ContainerBuilder>(this.ConfigureContainer);
}
public static ToolBoxBuilder Create(
string configName,
string[] arguments)
{
return new ToolBoxBuilder(configName, arguments);
}
/// <summary>
/// Constructs a tool box, a collection of tools.
/// </summary>
/// <returns>The constructed toolbox.</returns>
public ToolBox Build()
{
// There is a Catch-22 with how System.CommandLine is build. It
// requires the root command to be passed into the constructor
// for the CommandLineBuilder but we can't create the root command
// until we build our host.
//
// To handle this, we build our first host builder (which doesn't
// know about the command line parser), use it to get the command
// and then pass it into the command-line version to create a
// second `HostBuilder` that we don't use.
IHost host = this.hostBuilder.Build();
ILifetimeScope container = host.Services.GetAutofacRoot()!;
// Create the root command and attach our globals to that.
RootCommand rootCommand = container.Resolve<RootCommand>();
this.config.Attach(rootCommand);
this.logging.Attach(rootCommand);
// Finish creating the command line builder so we can make the parser.
CommandLineBuilder? builder = new CommandLineBuilder(rootCommand)
.UseDefaults()
.UseHost();
// Finish building the parser, wrap it in a tool box, and return it.
Parser parser = builder.Build();
var toolBox = new ToolBox(this.arguments, parser);
return toolBox;
}
/// <summary>
/// Provides additional configuration for Autofac containers.
/// </summary>
/// <param name="configure">The configuration method.</param>
/// <returns>The builder to chain methods.</returns>
public ToolBoxBuilder ConfigureContainer(
Action<ContainerBuilder> configure)
{
this.hostBuilder.ConfigureContainer(configure);
return this;
}
/// <summary>
/// Provides additional configuration for services.
/// </summary>
/// <param name="configure">The configuration method.</param>
/// <returns>The builder to chain methods.</returns>
public ToolBoxBuilder ConfigureServices(
Action<IServiceCollection> configure)
{
this.hostBuilder.ConfigureServices(configure);
return this;
}
private void ConfigureAppConfiguration(
HostBuilderContext context,
IConfigurationBuilder builder)
{
builder.SetBasePath(Directory.GetCurrentDirectory());
this.config.Configure(builder, this.arguments);
}
private void ConfigureContainer(
HostBuilderContext context,
ContainerBuilder builder)
{
// We want to get logging up and running as soon as possible. We
// also hook up the logging to the process exit in an attempt to
// make sure the logger is properly flushed before exiting.
builder.RegisterInstance(Log.Logger).As<ILogger>().SingleInstance();
AppDomain.CurrentDomain.ProcessExit +=
(_, _) => Log.CloseAndFlush();
// Register the global services as singletons.
builder
.RegisterInstance(this.config)
.AsSelf()
.SingleInstance();
builder
.RegisterInstance(this.logging)
.AsSelf()
.SingleInstance();
// Register the components required to make the CLI work.
builder.RegisterModule<ToolBuilderModule>();
}
private void ConfigureDefaultServiceProvider(
HostBuilderContext context,
ServiceProviderOptions options)
{
bool isDevelopment = context.HostingEnvironment.IsDevelopment();
options.ValidateScopes = isDevelopment;
options.ValidateOnBuild = isDevelopment;
}
private void ConfigureServices(
HostBuilderContext context,
IServiceCollection services)
{
services.AddAutofac();
}
}
}