← All tools

vvka-141/portico

Popularity 75 Updated Development & Build

Contract-first CLI framework for .NET: attributed routes, executable examples, Roslyn analyzers, and optional DI and Generic Host integration.

githubauto-collected

Installation

A directly usable install command is not verified yet. Check the project documentation or releases.

<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: