4

I can not figure out how to access the command line args in my ConsoleHostedService implementation class. I see in the sources CreateDefaultBuilder(args) somehow adds it to the configuration... named Args...

Having the main program:

internal sealed class Program
{
    private static async Task Main(string[] args)
    {
        await Host.CreateDefaultBuilder(args)
            .UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
            .ConfigureServices((context, services) =>
            {
                services.AddHostedService<ConsoleHostedService>();
            })
            .RunConsoleAsync();
    }
}

and the hosted service:

internal sealed class ConsoleHostedService : IHostedService
{
    public ConsoleHostedService(
        IHostApplicationLifetime appLifetime,
        IServiceProvider serviceProvider)
    {
        //...
    }
}
g.pickardou
  • 32,346
  • 36
  • 123
  • 268
  • 8
    `System.Environment.GetCommandLineArguments()`? – Dai Mar 01 '21 at 17:29
  • It depends on what you are actually trying to extract from command line. Provide an example of the use case – Nkosi Mar 01 '21 at 17:35
  • @Nkosi: I have the feeling *it definitely should not*. I would like to access the args array *as it is* – g.pickardou Mar 01 '21 at 17:46
  • @Dai Thx, this will be the backup plan, although probably this is not the .NET intended way, unless there would not have the `CreateDefaultBuilder(args)` called exactly the `main`'s `args`. Also the `Environment.CommandLine` is a monolitic string, of course I can split it by space, but it *would be less error prone* and more compatible if I got the original OS array – g.pickardou Mar 01 '21 at 17:55
  • @g.pickardou If you want the args array as is then create a model with a string[] property, initialize the model with the args and add it to the service collection so that it can be injected where needed – Nkosi Mar 01 '21 at 17:59
  • 1
    The one passed to configuration is to allow for configuration binding to strong types via the CommandLineConfigurationProvider https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.1#command-line-configuration-provider – Nkosi Mar 01 '21 at 18:01

2 Answers2

4

I don't believe there's a built-in DI method to get command-line arguments - but probably the reason that handling command-line arguments is the responsibility of your host application and that should be passing host/environment information in via IConfiguration and IOptions etc.

Anyway, just define your own injectables:

public interface IEntrypointInfo
{
    String CommandLine { get; }

    IReadOnlyList<String> CommandLineArgs { get; }

    // Default interface implementation, requires C# 8.0 or later:
    Boolean HasFlag( String flagName )
    {
        return this.CommandLineArgs.Any( a => ( "-" + a ) == flagName || ( "/" + a ) == flagName );
    }
}

/// <summary>Implements <see cref="IEntrypointInfo"/> by exposing data provided by <see cref="System.Environment"/>.</summary>
public class SystemEnvironmentEntrypointInfo : IEntrypointInfo
{
    public String CommandLine => System.Environment.CommandLine;

    public IReadOnlyList<String> CommandLineArgs => System.Environment.GetCommandLineArgs();
}

/// <summary>Implements <see cref="IEntrypointInfo"/> by exposing provided data.</summary>
public class SimpleEntrypointInfo : IEntrypointInfo
{
    public SimpleEntrypointInfo( String commandLine, String[] commandLineArgs )
    {
        this.CommandLine = commandLine ?? throw new ArgumentNullException(nameof(commandLine));
        this.CommandLineArgs = commandLineArgs ?? throw new ArgumentNullException(nameof(commandLineArgs));
    }

    public String CommandLine { get; }

    public IReadOnlyList<String> CommandLineArgs { get; }
}

//

public static class Program
{
    public static async Task Main( String[] args )
    {
        await Host.CreateDefaultBuilder( args )
            .UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
            .ConfigureServices((context, services) =>
            {
                services.AddHostedService<ConsoleHostedService>();
                services.AddSingleton<IEntrypointInfo,SystemEnvironmentEntrypointInfo>()
            })
            .RunConsoleAsync();
    }

For automated unit and integration tests, use SimpleEntrypointInfo.

Dai
  • 141,631
  • 28
  • 261
  • 374
2

The CreateDefaultBuilder adds a CommandLineConfigurationProvider to its configuration providers but you wouldn't normally access it directly. Instead, add an IConfiguration parameter to your ConsoleHostedService constructor and you will automatically receive settings from several settings sources, including command line arguments:

internal sealed class ConsoleHostedService : IHostedService
{
    public ConsoleHostedService(
        IHostApplicationLifetime appLifetime,
        IServiceProvider serviceProvider,
        IConfiguration configuration)
    {
        // Get the value as a string
        string argValueString = configuration["MyFirstArg"]

        // Or if it's an integer
        int argValueInt = configuration.GetValue<int>("MyFirstArg")
    }
}

This does require that your command line arguments follow the prescribed format as defined here:

MyFirstArg=12345
/MyFirstArg 12345
--MyFirstArg 12345

However...

If you really must get the actual command line args and, if you don't mind relying on the implementation of the default builder, you could do this:

Create a custom CommandLineConfigurationProvider class and expose its Data and Args properties:

public class ExposedCommandLineConfigurationProvider : CommandLineConfigurationProvider
{
    public ExposedCommandLineConfigurationProvider(IEnumerable<string> args, IDictionary<string, string> switchMappings = null)
        :base(args, switchMappings)
    {
    }

    public new IDictionary<string, string> Data => base.Data;

    public new IEnumerable<string> Args => base.Args;
}

Then in your main program, add it to the lst of existing configuration providers:

internal sealed class Program
{
    private static async Task Main(string[] args)
    {
        await Host.CreateDefaultBuilder(args)
            .UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))
            .ConfigureAppConfiguration(config => config
                .Add(new ExposedCommandLineConfigurationSource { Args = args }))
            .ConfigureServices((context, services) => services
                .AddHostedService<SchedulerWorker>())
            .RunConsoleAsync();
    }
}

Finally, dig your arguments provider out of the IConfiguration provided to you ConsoleHostedService:

internal sealed class ConsoleHostedService : IHostedService
{
    public ConsoleHostedService(
        IHostApplicationLifetime appLifetime,
        IServiceProvider serviceProvider,
        IConfiguration configuration)
    {
        if (configuration is ConfigurationRoot configRoot)
        {
            var provider = configRoot.Providers.OfType<ExposedCommandLineConfigurationProvider>().Single();
            var rawArgs = provider.Args;
            var namedArgs = provider.Data;
        }
        else
        {
            // Handle this unlikely situation
        }
    }
}

But for something that can be otherwise done so simply, that seems like a lot of work (and potentially breakable by any changes in the implementation of the default builder).

stritch000
  • 355
  • 4
  • 10