<img src="eng/brand/portico-512.png" alt="" width="88" align="left" hspace="16" vspace="4"
Portico
The command surface for .NET backend services.
Your service's operational surface is an API. Treat it like one.
ASP.NET Core for the terminal: your routes are routes, and your examples are executable tests — so the CLI cannot lie about what it accepts. One CliContractValidator<T test runs every [CliCommandExample] through the real pipeline, and a stale one fails the build. Roslyn analyzers check the rest at compile time. Zero dependencies. DI is opt-in.
using Portico;
public interface IAdminTool
{
[CliRoute("db migrate")]
[CliCommandExample("db migrate --connection-string \"Host=db\"")]
int Migrate([CliOption("--connection-string|-c", Sensitive = true)] string connectionString);
}
public sealed class AdminTool : IAdminTool
{
public int Migrate(string connectionString)System.Console.WriteLine("applied 3 migrations."); return 0; } }
public static class Program { public static int Main(string[] args) = CliApplication.Create(cfg = cfg.AddCommands(new AdminTool())).Run(args); }
That is the whole framework: a plain C# method, one route attribute, one example.
dotnet add package Portico
Or start from the template — a runnable CLI whose contract test is already green:
dotnet new install Portico.Templates dotnet new portico-cli -n MyCli && cd MyCli && dotnet test
---
## Your examples are tests
`[CliCommandExample]` is not a comment. `CliContractValidator<T>` runs every example through the real
pipeline against a `DispatchProxy` of your interface — one test case per example:
public static IEnumerable<object[] Examples() = new CliContractValidator<IAdminTool().Enumerate().Select(e = new object[] { e });
[Theory] [MemberData(nameof(Examples))] public void Dispatch(CliContractExample example) = Assert.True(example.Matched, $"Example did not dispatch: {example.Example}\n Reason: {example.FailureReason}");
Rename a route, make an argument required — the example stops dispatching and the build goes red,
and it tells you why in the framework's own words:
Example did not dispatch: pay --amount abc Reason: Value 'abc' for option '--amount' is invalid. abc is not a valid value for Decimal.
But dispatching is the floor, not the ceiling. Each example also reports **which handler it
reached** and **what values were bound to it**, so an example can pin the whole contract: