How can I access the command line arguments in a console application using IHostedService?
Asked Answered
A

2

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)
    {
        //...
    }
}
Aparri answered 1/3, 2021 at 17:25 Comment(6)
System.Environment.GetCommandLineArguments()?Plata
It depends on what you are actually trying to extract from command line. Provide an example of the use caseEnzootic
@Nkosi: I have the feeling it definitely should not. I would like to access the args array as it isAparri
@Plata 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 arrayAparri
@Aparri 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 neededEnzootic
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/…Enzootic
P
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.

Plata answered 1/3, 2021 at 18:52 Comment(1)
How constructor of SimpleEntrypointInfo will get data for it's parameters ? As those parameters are not injected anywhere in this codeTanah
F
7

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

Faires answered 9/2, 2023 at 1:34 Comment(0)
P
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.

Plata answered 1/3, 2021 at 18:52 Comment(1)
How constructor of SimpleEntrypointInfo will get data for it's parameters ? As those parameters are not injected anywhere in this codeTanah

© 2022 - 2024 — McMap. All rights reserved.