.NET 11 adds Process.RunAndCaptureTextAsync to start a local executable, collect its standard output and standard error, and wait for its exit code in one call.
Use it for commands with a bounded text response.
A child can write a warning to standard error and still exit successfully.
Why Read Both Output Streams at Once?
An export job calls a CSV checker before uploading a file.
A file with the header id,total and no data rows passes the check with a warning. A file with the wrong header should stop the upload.
Both cases write to standard error, so the parent needs the exit code to tell them apart.
A common implementation redirects both output streams, reads standard output to the end, then reads standard error. If the child fills its standard-error pipe, it can block before it closes standard output. The parent keeps waiting for that first read to finish. The new method drains both pipes while the child runs.
Call the CSV Checker
Create a console project for the export job.
The executable will launch a second copy of itself with --check, so you can run the example on Windows, Linux, or macOS without a separate CSV tool.
dotnet new console -n ExportCheck -f net11.0
cd ExportCheck
Save the following as Program.cs.
When you launch the checker with dotnet run, Environment.ProcessPath points to the project's generated executable.
If you launch a DLL via dotnet ExportCheck.dll instead, the process path points to the dotnet host and you would have to pass the DLL path as its first argument.
using System.ComponentModel;
using System.Diagnostics;
if (args is ["--check", var file])
{
using var reader = File.OpenText(file);
if (reader.ReadLine() != "id,total")
{
Console.Error.WriteLine("Expected CSV header: id,total");
return 2;
}
Console.WriteLine("Header OK");
if (reader.ReadLine() is null)
Console.Error.WriteLine("Warning: export contains no rows.");
return 0;
}
if (args is not [var path])
{
Console.Error.WriteLine("Usage: ExportCheck <csv-file>");
return 1;
}
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var result = await Process.RunAndCaptureTextAsync(
Environment.ProcessPath!, ["--check", path], deadline.Token);
Console.Write(result.StandardOutput);
Console.Error.Write(result.StandardError);
if (result.ExitStatus.Canceled)
{
Console.Error.WriteLine("Check timed out.");
return 1;
}
Console.WriteLine($"Child exit code: {result.ExitStatus.ExitCode}");
return result.ExitStatus.ExitCode;
}
catch (OperationCanceledException) when (deadline.IsCancellationRequested)
{
Console.Error.WriteLine("Check timed out.");
return 1;
}
catch (Win32Exception ex)
{
Console.Error.WriteLine($"Could not start the checker: {ex.Message}");
return 1;
}
This overload redirects both streams for you.
It passes path as one argument, even when the filename contains spaces.
If you use the overload that takes a ProcessStartInfo to set a working directory or environment variable, set UseShellExecute = false, RedirectStandardOutput = true, and RedirectStandardError = true.
Which Result Should Fail the Job?
In a Unix shell, start with a file containing only the expected header. Save stdout and stderr separately.
printf 'id,total\n' > 'order export.csv'
dotnet run -- 'order export.csv' > success.out 2> success.err
cat success.out
cat success.err
The parent writes Header OK and Child exit code: 0 to standard output.
The diagnostic Warning: export contains no rows. goes to standard error.
A parent that fails every time StandardError contains text would reject this file even though its header passed.
Now change the first column name.
printf 'order_id,total\n' > bad.csv
dotnet run -- bad.csv > failure.out 2> failure.err
echo "$?"
cat failure.err
The shell prints 2, the code the child assigned to a bad header.
The diagnostic now reads Expected CSV header: id,total.
A different executable may assign a different meaning to the same number.
For example, a tool can use a nonzero exit code to report that it found differences.
What If the Deadline Expires?
If the five-second deadline expires while the parent is reading the pipes, RunAndCaptureTextAsync attempts to kill the child and throws OperationCanceledException.
A child can also close both output streams and continue doing work.
If cancellation arrives while the helper is waiting for that child to exit, it can return a result with ExitStatus.Canceled set.
The parent checks both outcomes so it can print Check timed out. and exit with code 1 in either case.
If the checker runs in a hosted background service, link the host's stopping token to the command deadline so shutting down the worker also cancels its wait.
When Should You Stream the Output?
Both captured strings remain in memory until your code releases the result.
The five-second deadline puts no limit on how many bytes the checker can write before it expires.
For a build that prints progress for several minutes, start a Process with both streams redirected and enumerate ReadAllLinesAsync.
Each ProcessOutputLine has a Content string and a StandardError flag, so you can forward each line as it arrives.
After the streams end, wait for the exit status as well.
Summary
For the CSV checker, keep the warning in the job log and use the child's exit code to decide whether to upload the file.
If a replacement checker prints a diagnostic for every row, read its output with ReadAllLinesAsync so a large import doesn't leave you holding the entire log in memory.
Frequently Asked Questions
How do you capture both stdout and stderr from a process in .NET 11?
Pass the executable name and a sequence of arguments to Process.RunAndCaptureTextAsync. The returned ProcessTextOutput exposes StandardOutput, StandardError, and ExitStatus after the child finishes.
Does RunAndCaptureTextAsync throw for a nonzero exit code?
A normal exit with a nonzero code produces a result. Inspect ExitStatus.ExitCode and apply the meaning defined by the child program. A startup failure, such as an executable that cannot be found, can throw before you get a result.
Can RunAndCaptureTextAsync stream live build output?
The one-shot method returns the complete output after the process finishes. Start a Process with both streams redirected and consume ReadAllLinesAsync for progress lines as they arrive.

