Skip to content

Host mode: BenchmarkHost

BenchmarkHost discovers benchmarks by scanning assemblies for [Benchmark]-decorated methods. It also parses command-line arguments, so you can filter, configure, and drive runs entirely from the terminal without recompiling.

This mode is designed for dedicated benchmark projects - a separate console project that you run against your library.

Minimal setup

1. Create a console project

bash
dotnet new console -n MyApp.Benchmarks
cd MyApp.Benchmarks
dotnet add package NBenchmark
dotnet add package NBenchmark.Console
dotnet add reference ../MyApp/MyApp.csproj

2. Write benchmark classes

csharp
using NBenchmark.Attributes;

public class StringBenchmarks
{
    [Benchmark(Baseline = true)]
    public string Concat() => "hello" + " " + "world";

    [Benchmark]
    public string Interpolate() => $"hello {"world"}";
}

3. Wire up the host

csharp
// Program.cs
using NBenchmark;
using NBenchmark.Console;
using NBenchmark.Attributes;

await BenchmarkHost.Create(args)
    .AddFromAssembly<StringBenchmarks>()
    .WithReporter(new ConsoleReporter())
    .WithProgress(new ConsoleBenchmarkProgress(200, 25))
    .RunAsync();

4. Run

bash
dotnet run
dotnet run -- --filter String*
dotnet run -- --reporter markdown --output ./results

Benchmark attributes

[Benchmark]

Marks a public instance method for measurement.

csharp
[Benchmark]
public int MyMethod() => DoWork();

[Benchmark]
public async Task MyAsyncMethod() => await DoWorkAsync();

[Benchmark]
public async Task<int> MyAsyncMethodWithResult() => await ComputeAsync();

Properties:

PropertyTypeDescription
BaselineboolMarks this method as the baseline for ratio/significance calculations.
Descriptionstring?Optional label shown in output when descriptions are present.
Iterationsint?Override the default iteration count for this method only.
WarmupIterationsint?Override the default warmup count for this method only.
csharp
[Benchmark(Baseline = true, Description = "current production implementation")]
public string CurrentImpl() => Production.DoWork();

[Benchmark(Description = "candidate replacement")]
public string NewImpl() => Candidate.DoWork();

[BenchmarkArguments]

Runs the benchmark once for each set of arguments. The method must accept parameters matching the argument types.

csharp
[BenchmarkArguments(10)]
[BenchmarkArguments(1_000)]
[BenchmarkArguments(100_000)]
[Benchmark]
public void Sort(int n)
{
    var arr = Enumerable.Range(0, n).Reverse().ToArray();
    Array.Sort(arr);
}

Each argument set becomes a separate benchmark entry in the output, named MethodName(arg1, arg2, ...).

Lifecycle attributes

These attributes control setup and teardown at the class and iteration level. All decorated methods must have no parameters.

AttributeRunsTiming
[BenchmarkSetup]Once before any benchmark in the classNot measured
[BenchmarkTeardown]Once after all benchmarks in the classNot measured
[BenchmarkIterationSetup]Before each individual iterationNot measured
[BenchmarkIterationTeardown]After each individual iterationNot measured
csharp
public class DatabaseBenchmarks
{
    private DbConnection _conn = null!;

    [BenchmarkSetup]
    public void OpenConnection() => _conn = new DbConnection(connectionString);

    [BenchmarkTeardown]
    public void CloseConnection() => _conn.Dispose();

    [BenchmarkIterationSetup]
    public void BeginTransaction() => _conn.BeginTransaction();

    [BenchmarkIterationTeardown]
    public void RollbackTransaction() => _conn.RollbackTransaction();

    [Benchmark]
    public void RunQuery() => _conn.Execute("SELECT COUNT(*) FROM orders");
}

Class requirements

NBenchmark instantiates benchmark classes using Activator.CreateInstance. The class must have a public parameterless constructor (the default for any class without explicit constructors).

csharp
// Works - implicit parameterless constructor
public class MyBenchmarks { ... }

// Works - explicit parameterless constructor
public class MyBenchmarks
{
    public MyBenchmarks() { /* setup */ }
}

// Does not work - no parameterless constructor
public class MyBenchmarks(IDatabase db) { ... }

Benchmark classes with dependencies

If you want benchmark classes to have constructor dependencies (a repository, a logger, an HttpClient, a DbContext, etc.), add the optional NBenchmark.DependencyInjection companion package and use UseDependencyInjection<T>:

csharp
using Microsoft.Extensions.DependencyInjection;
using NBenchmark.DependencyInjection;

var services = new ServiceCollection()
    .AddSingleton<IOrderRepository, SqlOrderRepository>()
    .AddTransient<OrderBenchmarks>()
    .BuildServiceProvider();

await BenchmarkHost.Create(args)
    .UseDependencyInjection<OrderBenchmarks>(services)
    .RunAsync();

public sealed class OrderBenchmarks(IOrderRepository repository)
{
    [Benchmark]
    public int CountOrders() => repository.Count();
}

The companion package resolves benchmark classes from the supplied IServiceProvider, so constructor dependencies are injected automatically. A scoped variant is also available for DbContext-style lifetimes. See the Dependency Injection guide for the full API, lifetime semantics, and how to plug in containers other than Microsoft.Extensions.DependencyInjection.

Scanning multiple assemblies

Call AddFromAssembly once per assembly:

csharp
BenchmarkHost.Create(args)
    .AddFromAssembly<StringBenchmarks>()
    .AddFromAssembly<DatabaseBenchmarks>()
    .AddFromAssembly(typeof(SomeOtherClass).Assembly)
    ...

Applying options

Use WithOptions to set defaults that the CLI can override:

csharp
BenchmarkHost.Create(args)
    .AddFromAssembly<MyBenchmarks>()
    .WithOptions(new MeasurementOptions
    {
        Iterations = 500,
        WarmupIterations = 50,
        MeasureAllocations = true,
        ConfidenceLevel = 0.99,
    })
    .WithReporter(new ConsoleReporter())
    .RunAsync();

CLI flags like --iterations always override WithOptions values.

By default benchmarks run in random order to reduce systematic bias. Call WithRunOrder(RunOrder.Declaration) (or pass --order declaration) to run them in declaration order instead.

Listing benchmarks without running

bash
dotnet run -- --list

Output:

── StringBenchmarks ──
    Concat - current production implementation
    Interpolate - candidate replacement
── DatabaseBenchmarks ──
    RunQuery

Dry run

Validates that all benchmarks compile, discover, and wire up correctly - without invoking the body:

bash
dotnet run -- --dry-run

--dry-run is implemented as --iterations 0 --warmup 0. The body is not invoked, and no measurements are taken. Use it to confirm discovery, setup, and instantiation work before a full run. To run the body exactly once for a smoke test, use --iterations 1 --warmup 0.

Return value

RunAsync returns IReadOnlyList<BenchmarkResult> with all results, including errored benchmarks. Exit code is 0 on success.

Next steps

Released under the MIT License.