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
dotnet new console -n MyApp.Benchmarks
cd MyApp.Benchmarks
dotnet add package NBenchmark
dotnet add package NBenchmark.Console
dotnet add reference ../MyApp/MyApp.csproj2. Write benchmark classes
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
// 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
dotnet run
dotnet run -- --filter String*
dotnet run -- --reporter markdown --output ./resultsBenchmark attributes
[Benchmark]
Marks a public instance method for measurement.
[Benchmark]
public int MyMethod() => DoWork();
[Benchmark]
public async Task MyAsyncMethod() => await DoWorkAsync();
[Benchmark]
public async Task<int> MyAsyncMethodWithResult() => await ComputeAsync();Properties:
| Property | Type | Description |
|---|---|---|
Baseline | bool | Marks this method as the baseline for ratio/significance calculations. |
Description | string? | Optional label shown in output when descriptions are present. |
Iterations | int? | Override the default iteration count for this method only. |
WarmupIterations | int? | Override the default warmup count for this method only. |
[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.
[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.
| Attribute | Runs | Timing |
|---|---|---|
[BenchmarkSetup] | Once before any benchmark in the class | Not measured |
[BenchmarkTeardown] | Once after all benchmarks in the class | Not measured |
[BenchmarkIterationSetup] | Before each individual iteration | Not measured |
[BenchmarkIterationTeardown] | After each individual iteration | Not measured |
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).
// 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>:
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:
BenchmarkHost.Create(args)
.AddFromAssembly<StringBenchmarks>()
.AddFromAssembly<DatabaseBenchmarks>()
.AddFromAssembly(typeof(SomeOtherClass).Assembly)
...Applying options
Use WithOptions to set defaults that the CLI can override:
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
dotnet run -- --listOutput:
── StringBenchmarks ──
Concat - current production implementation
Interpolate - candidate replacement
── DatabaseBenchmarks ──
RunQueryDry run
Validates that all benchmarks compile, discover, and wire up correctly - without invoking the body:
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
- CLI Reference - all command-line flags
- Configuration - options reference
- Reporters - all reporters
