This guide consolidates every breaking change shipping in 3.0.0.
The breaking-change set is final. Section 8 (DI registration split) was added in the late 3.0.0 betas and is the last change to this guide; it covers every change shipping in 3.0.0.
| Removed / changed | Migrate to |
|---|---|
CliRun.UseExternalProcessFactory |
IProcessInvoker (or DI) |
CliRun.UseFilePathResolver |
IProcessInvoker (or DI) |
ExitConfiguration setter |
Pass ProcessExitConfiguration to the constructor |
ProcessInvoker(factory, items) |
ProcessInvoker(factory, middleware, items) |
ProcessInvoker(factory, middleware) |
ProcessInvoker(factory, middleware, items: null) |
ExternalProcess(resolver, targetPath) |
ExternalProcess(resolver, configuration, exitConfig) |
ExternalProcess(config, exitConfig) |
ExternalProcess(resolver, configuration, exitConfig) |
Subclassing ExternalProcess |
Not supported — the class is sealed |
Subclassing ProcessConfigurationBuilder |
Not supported — the class is sealed |
ProcessConfigurationFactory removed |
Construct ProcessConfiguration directly with init setters |
TargetFilePath becomes required |
[SetsRequiredMembers] on the convenience constructor |
ArgumentsList merged into init-only ArgumentList |
Set ArgumentList property via init setter |
Working-directory validation moved into ProcessConfiguration construction |
Throws on non-existent directory at init time |
| Builder kept, positioned advanced | Argument escaping, UserCredentialSpec and resource-policy callback flows |
ProcessConfigurationBuilder init setters delegate where applicable; DI registration unchanged |
Use builder for advanced scenarios only |
AddCliInvoke moved to the CliInvoke package (late beta) |
Same namespace; chain AddCliInvokeSpecializations() for PowerShell/Cmd middleware |
CliRun is now statelessCliRun.UseExternalProcessFactory and CliRun.UseFilePathResolver have been
removed along with all backing static state. CliRun is a batteries-included
defaults facade: every Run*/FireAndForget call allocates a fresh
ProcessInvocationPipeline (and a fresh ExternalProcessFactory with a default
FilePathResolver) per call.
Before (v2.x):
CliRun.UseExternalProcessFactory(myFactory);
CliRun.UseFilePathResolver(myResolver);
ProcessResult result = await CliRun.RunAsync("dotnet", "--version");
After (3.0.0):
// For custom factories/resolvers, use IProcessInvoker directly (or DI):
IExternalProcessFactory factory = myFactory;
IProcessInvoker invoker = new ProcessInvoker(factory);
ProcessResult result = await invoker.ExecuteAsync(
new ProcessConfiguration("dotnet", "--version"));
No [Obsolete] shim or bridge method was added — this is a direct cutover.
InvocationContext.Result / .Middleware ownershipNo API change. The Result and Middleware properties are owned by specific
middleware. The only legitimate mutators are the MiddlewareChain walker, the
terminal delegate that bridges the chain to the pipeline, and any propagating
middleware that short-circuits the chain. Do not read or write these properties
outside those mutators; Result is null until the chain completes.
ExitConfiguration is read-onlyIExternalProcess.ExitConfiguration and ExternalProcess.ExitConfiguration are
now read-only { get; } and supplied at construction. There is no WithExitConfiguration
method.
Before (v2.x):
ExternalProcess process = /* ... */;
process.ExitConfiguration = ProcessExitConfiguration.CreateGraceful();
After (3.0.0):
IFilePathResolver resolver = new FilePathResolver();
ExternalProcess process = new ExternalProcess(
resolver,
configuration,
ProcessExitConfiguration.CreateGraceful());
ProcessInvoker has two constructorsThe two partial overloads were removed.
| Removed | Use |
|---|---|
ProcessInvoker(factory, MiddlewareItems?) |
ProcessInvoker(factory, Array.Empty<IProcessMiddleware>(), items) |
ProcessInvoker(factory, IEnumerable<IProcessMiddleware>) |
ProcessInvoker(factory, middleware, items: null) |
The surviving constructors are ProcessInvoker(IExternalProcessFactory) and
ProcessInvoker(IExternalProcessFactory, IEnumerable<IProcessMiddleware>, MiddlewareItems?).
DI configure bindings and the PowershellProcessInvoker / CmdProcessInvoker
specializations now use the four-argument form with sharedItems: null.
ExternalProcess keeps only constructor CExternalProcess is now (IFilePathResolver, ProcessConfiguration, ProcessExitConfiguration?).
The (IFilePathResolver, string) and (ProcessConfiguration, ProcessExitConfiguration?)
constructors were removed.
After (3.0.0):
IFilePathResolver resolver = new FilePathResolver();
using ExternalProcess process = new ExternalProcess(resolver, configuration);
The class is also sealed, so it can no longer be subclassed.
ProcessConfigurationBuilder is sealedProcessConfigurationBuilder is now sealed. Fluent chaining via the
IProcessConfigurationBuilder interface is unaffected. If you subclassed the
builder, switch to composing an IProcessConfigurationBuilder instead.
ProcessResult equality is symmetricProcessResult.Equals(object?) now performs exact runtime-type matching so that
a.Equals(b) == b.Equals(a) holds across the ProcessResult and
BufferedProcessResult hierarchy. ProcessResult remains unsealed in this
release.
Breaking change:
PipedProcessResultwas removed in 3.0.0. It previously held liveStandardOutput/StandardErrorstreams and was disposable. For streaming output, useIExternalProcess, which exposes the liveProcesswithStandardOutput/StandardErrorstreams, stdin, and lifecycle control. TheExecutePipedAsync/RunPipedAsync/CapturePipedResultAsyncmethods no longer exist.
AddCliInvoke ships in the main CliInvoke package (late beta)AddCliInvoke and its service registrations moved from CliInvoke.Specializations into the main
CliInvoke package. The namespace is unchanged (CliInvoke.Extensions), so using CliInvoke.Extensions;
call sites keep working.
Before (early 3.0.0 betas):
// CliInvoke.Specializations defined the registration entry point
services.AddCliInvoke(builder => builder.UsePowerShell());
After (3.0.0):
// AddCliInvoke ships in the main CliInvoke package:
services.AddCliInvoke(builder => builder.UsePowerShell())
.AddCliInvokeSpecializations(); // registers the PowerShell/Cmd/DefaultShell middleware types
AddCliInvoke (either overload) registers the core services and the core built-in middleware
(LoggingMiddleware, RetryMiddleware).AddCliInvokeSpecializations (CliInvoke.Specializations package) registers only
PowerShellMiddleware, CmdMiddleware, DefaultShellMiddleware, and ShellMiddlewareOptions.ServiceLifetime — middleware lifetimes are matched to the invoker lifetime to avoid captive
scoped-in-singleton dependencies.AddCliInvoke(builder => builder.UsePowerShell()) without AddCliInvokeSpecializations()
compiles but throws InvalidOperationException ("No service of type ... PowerShellMiddleware ...")
when the invoker is first resolved. The same applies to UseCmd() and UseDefaultShell().ProcessConfiguration and ExternalProcess are immutable after constructionIn v3, ProcessConfiguration is never mutated after construction.
Previously, ExternalProcess.Start() / StartAsync() would rewrite
Configuration.TargetFilePath with the runtime-resolved file path. That
mutation is gone — the caller's Configuration instance stays unchanged
for the lifetime of the process.
| Property | v2 | v3 |
|---|---|---|
ProcessConfiguration.TargetFilePath |
set |
init |
ExternalProcess.Configuration / IExternalProcess.Configuration |
set |
init |
Post-construction assignment to any of these is now a compile error.
Before (v2.x):
ProcessConfiguration config = new ProcessConfiguration("dotnet");
config.TargetFilePath = @"C:\resolved\dotnet.exe"; // compiled in v2
ExternalProcess process = new ExternalProcess(resolver, config);
process.Configuration = new ProcessConfiguration("dotnet"); // compiled in v2
After (3.0.0):
// TargetFilePath is init-only — set it at construction:
ProcessConfiguration config = new ProcessConfiguration("dotnet")
{
TargetFilePath = @"C:\resolved\dotnet.exe"
};
// ExternalProcess.Configuration is init-only — pass via constructor:
ExternalProcess process = new ExternalProcess(resolver, config);
IFilePathResolver dropped from PowerShell specializationsThe IFilePathResolver parameter was removed from three PowerShell-related
constructors. File resolution is now handled internally by ExternalProcess
at start time.
| Type | v2 constructor | v3 constructor |
|---|---|---|
PowershellProcessConfiguration |
(IFilePathResolver, string, bool) |
(string, bool) |
PowershellProcessInvoker |
(IFilePathResolver, IExternalProcessFactory) |
(IExternalProcessFactory) |
PowerShellMiddleware |
(IFilePathResolver) |
(ShellMiddlewareOptions?) |
The resolved file path is no longer written back to
Configuration.TargetFilePath. Instead, use the ExecutedFilePath
property on the result objects:
ProcessResult.ExecutedFilePath — the resolved path that was
actually executed.BufferedProcessResult.ExecutedFilePath — same, for buffered
results.IFilePathResolver resolver = new FilePathResolver();
ExternalProcess process = new ExternalProcess(resolver, new ProcessConfiguration("dotnet"));
process.Start();
BufferedProcessResult result = await process.CaptureBufferedResultAsync(CancellationToken.None);
string resolvedPath = result.ExecutedFilePath;
// e.g. "C:\Program Files\dotnet\dotnet.exe"
// Configuration.TargetFilePath is still "dotnet" (unchanged):
string originalPath = process.Configuration.TargetFilePath;
// "dotnet"
v2-style code is defined in GLOSSARY.md as code that uses prior-major-version APIs or defaults to v3-advanced construction styles.
In v3, ProcessConfiguration is directly constructible via init setters with a
required TargetFilePath. A convenience constructor marked [SetsRequiredMembers]
covers the common case. The builder is reserved for advanced scenarios (argument
escaping, UserCredentialSpec, resource-policy callback flows).
Before (v2-style):
// v2: ProcessConfigurationFactory or mutable TargetFilePath
ProcessConfiguration config = new ProcessConfiguration("dotnet")
{
TargetFilePath = "dotnet",
Arguments = "--version"
};
After (v3):
// v3: convenience constructor — TargetFilePath is required
ProcessConfiguration config = new ProcessConfiguration("dotnet", "--version");
// v3: object initializer — TargetFilePath is required via init setter
ProcessConfiguration config = new ProcessConfiguration
{
TargetFilePath = "dotnet",
Arguments = "--version"
};
// v3: ArgumentList replaces the old ArgumentsList property
ProcessConfiguration config = new ProcessConfiguration("dotnet")
{
ArgumentList = ["--list-sdks", "--verbosity", "minimal"]
};
// v3: working-directory validation throws at construction time
ProcessConfiguration config = new ProcessConfiguration("dotnet", "--version")
{
WorkingDirectoryPath = "/nonexistent" // throws DirectoryNotFoundException
};
CliRun Static-Call UsersBefore (v2.x):
// v2: CliRun with factory/resolver configuration
CliRun.UseExternalProcessFactory(myFactory);
CliRun.UseFilePathResolver(myResolver);
ProcessResult result = await CliRun.RunAsync("dotnet", "--version");
After (v3):
// v3: CliRun is stateless — use IProcessInvoker for custom factories
IProcessInvoker invoker = new ProcessInvoker(myFactory);
ProcessResult result = await invoker.ExecuteAsync(
new ProcessConfiguration("dotnet", "--version"));
IProcessInvoker UsersBefore (v2.x):
// v2: ProcessInvoker with two-arg constructor
ProcessInvoker invoker = new ProcessInvoker(factory, middleware);
BufferedProcessResult result = await invoker.ExecuteBufferedAsync(
new ProcessConfiguration("dotnet", "--version"));
After (v3):
// v3: ProcessInvoker requires three arguments
ProcessInvoker invoker = new ProcessInvoker(factory, middleware, null);
BufferedProcessResult result = await invoker.ExecuteBufferedAsync(
new ProcessConfiguration("dotnet", "--version"));
IExternalProcess UsersBefore (v2.x):
// v2: ExternalProcess with two-arg constructor and mutable ExitConfiguration
IFilePathResolver resolver = new FilePathResolver();
ExternalProcess process = new ExternalProcess(resolver, "dotnet");
process.ExitConfiguration = ProcessExitConfiguration.CreateGraceful();
await process.StartAsync(CancellationToken.None);
await process.WaitForExitOrTimeoutAsync(CancellationToken.None);
After (v3):
// v3: ExternalProcess is sealed, constructor C only, ExitConfiguration at construction
IFilePathResolver resolver = new FilePathResolver();
using ExternalProcess process = new ExternalProcess(
resolver,
new ProcessConfiguration("dotnet", "--version"),
ProcessExitConfiguration.CreateGraceful());
await process.StartAsync(CancellationToken.None);
await process.WaitForExitOrTimeoutAsync(CancellationToken.None);