ConsoleRunnerLib 1.2.2
dotnet add package ConsoleRunnerLib --version 1.2.2
NuGet\Install-Package ConsoleRunnerLib -Version 1.2.2
<PackageReference Include="ConsoleRunnerLib" Version="1.2.2" />
<PackageVersion Include="ConsoleRunnerLib" Version="1.2.2" />
<PackageReference Include="ConsoleRunnerLib" />
paket add ConsoleRunnerLib --version 1.2.2
#r "nuget: ConsoleRunnerLib, 1.2.2"
#:package ConsoleRunnerLib@1.2.2
#addin nuget:?package=ConsoleRunnerLib&version=1.2.2
#tool nuget:?package=ConsoleRunnerLib&version=1.2.2
ConsoleRunnerLib
The modern and easiest Console Runner adapted to DI principles.
Combine the power of Console apps with cancellation capability and custom logging.
How to install
Via the Package Manager:
Install-Package ConsoleRunnerLib
Via the .NET CLI
dotnet add package ConsoleRunnerLib
How to use via DI
The common way to use the library is to add it to the ServiceCollection of the host builder:
IHost Host.CreateDefaultBuilder().ConfigureServices((context,services) =>
{
services
//we assume that the Driver will use the library (example given later)
.AddScoped<Driver>()
//this adds the implementations for IConsoleRunner, IConsoleLogger
.AddConsoleRunnerLib();
}).Build();
IConsoleRunner
The IConsoleRunner contains exactly 3 methods to run a console application:
public interface IConsoleRunner
{
//The events are used only when the RunWithEvents is called
event EventHandler<DataReceivedEventArgs>? ErrorReceived;
event EventHandler<DataReceivedEventArgs>? OutputReceived;
Task<string?> Run(string executablePath, string arguments, string workingDirectory = "");
Task<ProcessOutput> RunAndGetOutputAndError(string executablePath, string arguments, string workingDirectory = "");
Task<int> RunWithEvents(string executablePath, string arguments, string workingDirectory = "", CancellationToken cancellationToken = default);
}
In general a console application will output messages in both the standard output and standard error streams.
If the output is expected to be minimal and we do not anticipate messages in the standard error stream, we can call the simple Run method.
In the example below, we aim to retrieve the title of a video file with the yt-dlp executable:
public class Driver
{
private readonly IConsoleRunner _process;
private readonly string _exePath = "c:/tools/yt-dlp.exe";
private readonly ILogger<Driver> _logger;
private readonly IServiceProvider _provider;
public Driver(IConsoleRunner process, ILogger<Driver> logger, IServiceProvider provider)
{
_process = process;
_logger = logger;
_provider = provider;
}
public async Task<string?> GetTitle(string videoId) =>
await _process.Run(_exePath, $" --get-title -- {videoId}");
}
If we expect output for the standard error stream, then we call the more generic RunAndGetOutputAndError method. The returned ProcessOutput struct is returned which is as follows:
public readonly struct ProcessOutput
{
public string StandardOutput { get; init; }
public string StandardError { get; init; }
public int ExitCode { get; init; }
}
Therefore we can inspect both the output and error streams, as well as the exit code of the console app, and act accordingly. For example:
ProcessOutput output = await _process.RunAndGetOutputAndError(_exePath, $" --get-title -- {videoId}");
if(output.StandardError.Length > 0 )
_logger.LogError(output.StandardError);
else
_logger.LogInformation(output.StandardOutput);
Long runs
For long runs, the RunWithEvents provides full control capability over the application.
private async Task RunProcessExample()
{
IConsoleRunner process = _provider.GetConsoleRunner();
process.OutputReceived += (object? s, DataReceivedEventArgs args) =>
{
string? status = args.Data;
if (string.IsNullOrWhiteSpace(status)) return;
//do something
_logger.LogInformation(status);
};
process.ErrorReceived += (object? s, DataReceivedEventArgs args) =>
{
string? status = args.Data;
if (string.IsNullOrWhiteSpace(status)) return;
//do something
_logger.LogError(status);
};
//...
//we assume arguments and workingDirectory are defined before
int exitCode = await process.RunWithEvents(_exePath, arguments, workingDirectory);
}
Cancel a long run
Let's modify the example above in order to allow cancellation capability. We need to add a CancellationToken as the last parameter and handle the TaskCancelledException that will be thrown when the cancellation is called:
private async Task RunProcessExample(CancellationToken cancellationToken)
{
IConsoleRunner process = _provider.GetConsoleRunner();
process.OutputReceived += (object? s, DataReceivedEventArgs args) =>
{
//...
};
process.ErrorReceived += (object? s, DataReceivedEventArgs args) =>
{
//...
};
//...
try
{
//we assume arguments and workingDirectory are defined before
int exitCode = await process.RunWithEvents(_exePath, arguments, workingDirectory, cancellationToken);
}
catch (TaskCancelledException)
{
_logger.LogInformation("Run was cancelled.")
}
}
In order to initiate the cancellation, the cancellation token source should be accessible to both the method that will run the process and the method that will call the Cancel operation.
CancellationTokenSource? cancelSource;
async Task Runner()
{
//...
cancelSource = new();
await driver.RunProcessExample(cancelSource!.Token);
}
//this method will cause the TaskCancelledException to be thrown
void CancelNow()
{
cancelSource?.Cancel();
}
Additional Examples
Working with Output and Error Streams Separately
When you need to handle both output and error streams with detailed exit code checking:
public class ProcessExecutor
{
private readonly IConsoleRunner _runner;
private readonly ILogger<ProcessExecutor> _logger;
public ProcessExecutor(IConsoleRunner runner, ILogger<ProcessExecutor> logger)
{
_runner = runner;
_logger = logger;
}
public async Task<bool> ExecuteCommand(string executable, string args)
{
try
{
ProcessOutput result = await _runner.RunAndGetOutputAndError(executable, args);
// Check for success before logging
if (result.ExitCode == 0)
{
_logger.LogInformation("Command succeeded:\n{Output}", result.StandardOutput);
return true;
}
else
{
_logger.LogError("Command failed with exit code {ExitCode}:\n{Error}",
result.ExitCode, result.StandardError);
return false;
}
}
catch (Exception ex)
{
_logger.LogCritical(ex, "Failed to execute command");
return false;
}
}
}
Real-time Monitoring with Event Streaming
For operations requiring real-time feedback (e.g., deployments, builds, data processing):
public class DeploymentMonitor
{
private readonly IConsoleRunner _runner;
private readonly ILogger<DeploymentMonitor> _logger;
public DeploymentMonitor(IConsoleRunner runner, ILogger<DeploymentMonitor> logger)
{
_runner = runner;
_logger = logger;
}
public async Task MonitorDeployment(string scriptPath, CancellationToken cancellationToken)
{
_runner.OutputReceived += (s, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data))
{
_logger.LogInformation("[DEPLOY] {Message}", e.Data);
}
};
_runner.ErrorReceived += (s, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data))
{
_logger.LogWarning("[DEPLOY-WARN] {Message}", e.Data);
}
};
try
{
int exitCode = await _runner.RunWithEvents(scriptPath, "", "", cancellationToken);
_logger.LogInformation("Deployment completed with exit code: {ExitCode}", exitCode);
}
catch (OperationCanceledException)
{
_logger.LogWarning("Deployment was cancelled by user");
}
}
}
Working with Different Directories
Execute commands in specific directories (useful for multi-repo or context-specific operations):
public class MultiPathExecutor
{
private readonly IConsoleRunner _runner;
public MultiPathExecutor(IConsoleRunner runner)
{
_runner = runner;
}
public async Task<string?> GetProjectVersion(string projectDirectory)
{
// Run in the project directory context
string? version = await _runner.Run(
"dotnet",
"--version",
projectDirectory
);
return version;
}
public async Task<ProcessOutput> RunBuildInDirectory(string projectPath)
{
return await _runner.RunAndGetOutputAndError(
"dotnet",
"build --configuration Release",
projectPath
);
}
}
Timeout Pattern with CancellationToken
Implement command timeouts by combining CancellationToken with Task.Delay:
public class TimedCommandExecutor
{
private readonly IConsoleRunner _runner;
private readonly ILogger<TimedCommandExecutor> _logger;
public TimedCommandExecutor(IConsoleRunner runner, ILogger<TimedCommandExecutor> logger)
{
_runner = runner;
_logger = logger;
}
public async Task<int> ExecuteWithTimeout(
string executable,
string args,
TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
try
{
return await _runner.RunWithEvents(executable, args, "", cts.Token);
}
catch (OperationCanceledException)
{
_logger.LogError("Command execution timed out after {Timeout}ms", timeout.TotalMilliseconds);
throw;
}
}
}
Performance Considerations
Runmethod: Use for simple, quick commands where you only care about standard output.RunAndGetOutputAndErrormethod: Optimal for commands that may produce error messages and where you need exit code validation.RunWithEventsmethod: Use for long-running operations or when you need real-time feedback. The event-based approach allows you to process output as it arrives without buffering the entire result.
Notes
- Process objects are properly disposed after execution completes
- All methods support working directory specification for context-specific execution
- CancellationToken is fully supported in
RunWithEventsfor graceful shutdown - Events are only available when using
RunWithEvents; the other methods buffer output for efficiency
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.