7

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)
    {
        //...
    }
}
6
  • 9
    System.Environment.GetCommandLineArguments()? Commented Mar 1, 2021 at 17:29
  • It depends on what you are actually trying to extract from command line. Provide an example of the use case Commented Mar 1, 2021 at 17:35
  • @Nkosi: I have the feeling it definitely should not. I would like to access the args array as it is Commented Mar 1, 2021 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 Commented Mar 1, 2021 at 17:55
  • 1
    The one passed to configuration is to allow for configuration binding to strong types via the CommandLineConfigurationProvider learn.microsoft.com/en-us/aspnet/core/fundamentals/… Commented Mar 1, 2021 at 18:01

2 Answers 2

8

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

Sign up to request clarification or add additional context in comments.

Comments

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.

1 Comment

How constructor of SimpleEntrypointInfo will get data for it's parameters ? As those parameters are not injected anywhere in this code

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.