BlazorAmpowering.Coverage 1.0.0

dotnet add package BlazorAmpowering.Coverage --version 1.0.0
                    
NuGet\Install-Package BlazorAmpowering.Coverage -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="BlazorAmpowering.Coverage" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BlazorAmpowering.Coverage" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="BlazorAmpowering.Coverage" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add BlazorAmpowering.Coverage --version 1.0.0
                    
#r "nuget: BlazorAmpowering.Coverage, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package BlazorAmpowering.Coverage@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=BlazorAmpowering.Coverage&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=BlazorAmpowering.Coverage&version=1.0.0
                    
Install as a Cake Tool

BlazorAmpowering.Coverage

Fody IL weaver that instruments every method in your .NET assemblies at compile time and feeds production code coverage into Prometheus / Mimir via OpenTelemetry — with zero runtime configuration and no external agent.


What is production code coverage?

Unit-test coverage tells you which lines a test suite exercises in a sandbox.
Production code coverage tells you which methods your real users actually trigger in the running application — a completely different question.

Typical findings:

  • 30–40 % of methods in a large application are never called in production, yet they are tested.
  • Frequently called methods are often undertested because the team did not know they were hot paths.
  • Legacy code hidden behind dead feature flags remains fully covered by CI but is never executed.

This package lets you answer those questions by injecting a lightweight counter into every method at build time. No profiler, no agent, no restart required — the data flows into the same Prometheus/Mimir pipeline you already have.


How Fody IL weaving works

Fody is a .NET build task that runs after the compiler but before the output assembly is written to disk. It reads the compiled IL with Mono.Cecil, modifies it in memory, and writes the patched assembly in place.

This weaver (BlazorAmpowering.Coverage) hooks into that pipeline to:

  1. Scan every method in the assembly.
  2. Assign each method a stable numeric ID (method_id) derived from an FNV-32 hash of the method's full signature. This ID is deterministic across builds as long as the signature does not change.
  3. Inject 4 IL instructions at the very top of each method body — before any user code runs:
ldsfld  <CoverageModuleInit>::s_baseSlot   // push the assembly's base offset (int32)
ldc.i4  <localSlot>                        // push this method's local index (int32)
add                                        // absolute slot = base + local
call    CoverageRegistry.Hit(int32)        // record the hit
  1. Generate a CLR module initializer ([ModuleInitializer]) that runs automatically when the assembly is first loaded by the CLR. This initializer calls CoverageRegistry.Register() to allocate slots in the shared registry and store the metadata (class name, method name, source file, line number) for every method.

The result: no reflection, no dictionary lookups, no string formatting on the hot path. The call overhead is a static field load, an integer addition, a bounds-checked array write, and an Interlocked.Increment.


Packages

Package Role
BlazorAmpowering.Coverage Fody weaver — add to every project you want to instrument
BlazorAmpowering.Coverage.Runtime Runtime registry + OTel/Prometheus export — entry project only

The weaver emits IL that calls into the runtime. The runtime package must be present in the application that loads the instrumented assemblies. The two packages are decoupled so your libraries do not carry a dependency on OTel — only the host application does.


Installation

Step 1 — Every project you want to instrument

Add the weaver package to the .csproj of each project (main app, service libraries, data layers, shared kernel, etc.):

<ItemGroup>
  <PackageReference Include="BlazorAmpowering.Coverage" Version="1.0.0" PrivateAssets="all" />
</ItemGroup>

PrivateAssets="all" marks the weaver as a build-time tool. It will not appear in the consuming project's output or be transitively required by downstream packages. Fody itself is a public transitive dependency (intentionally, so the MSBuild integration is available in consumer projects without extra steps).

Then create a FodyWeavers.xml file at the root of the same project (next to the .csproj):

<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <BlazorAmpowering.Coverage />
</Weavers>

Why is this file required? Fody uses FodyWeavers.xml as the explicit list of weavers to run for a given project. The element name (<BlazorAmpowering.Coverage />) must match the NuGet package ID exactly. Without this file, Fody is installed but does nothing. The .xsd reference is optional — it enables IDE autocomplete for the configuration attributes.

This file must be present in every project that references the weaver package.

Step 2 — Entry project only

Add the runtime to the project that starts the application:

<ItemGroup>
  <PackageReference Include="BlazorAmpowering.Coverage.Runtime" Version="1.0.0" />
</ItemGroup>

Register the OTel pipeline in Program.cs:

builder.AddProductionCoverage();

Place this call after builder.AddBlazorTelemetry() if you are also using BlazorAmpowering.Observability, so both pipelines share the same OTel meter provider.


Multi-project solution example

MySolution/
├── MySolution.Web/                  ← Blazor Server / Web API — the entry project
│   ├── MySolution.Web.csproj            ← BlazorAmpowering.Coverage (weaver, PrivateAssets=all)
│   │                                        BlazorAmpowering.Coverage.Runtime
│   ├── FodyWeavers.xml                  ← <BlazorAmpowering.Coverage />
│   └── Program.cs                       ← builder.AddProductionCoverage()
│
├── MySolution.Services/             ← Business service library
│   ├── MySolution.Services.csproj       ← BlazorAmpowering.Coverage (weaver, PrivateAssets=all)
│   └── FodyWeavers.xml                  ← <BlazorAmpowering.Coverage />
│
├── MySolution.Data/                 ← Data access layer
│   ├── MySolution.Data.csproj           ← BlazorAmpowering.Coverage (weaver, PrivateAssets=all)
│   └── FodyWeavers.xml                  ← <BlazorAmpowering.Coverage />
│
└── MySolution.Domain/               ← Domain model / shared kernel
    ├── MySolution.Domain.csproj         ← BlazorAmpowering.Coverage (weaver, PrivateAssets=all)
    └── FodyWeavers.xml                  ← <BlazorAmpowering.Coverage />

At runtime, each assembly registers itself independently with CoverageRegistry. The registry assigns each assembly a base offset so slot indices never collide across assemblies. All methods — regardless of which project they come from — appear in Prometheus under the same metric names with no coordination required between assemblies.


What gets instrumented

The weaver instruments every non-abstract, non-external method it encounters:

  • Instance methods, static methods, constructors, finalizers
  • Generic method instantiations (each closed form is a distinct slot)
  • Async state machine MoveNext() methods — the actual body of every async method
  • Property getters and setters
  • Local functions compiled by the C# compiler

The following are intentionally skipped:

  • Abstract and interface methods (no body)
  • P/Invoke extern methods
  • The generated <CoverageModuleInit> class itself (to avoid recursion)
  • Methods flagged with [CompilerGenerated] that are not async state machines (e.g., display classes for closures are covered indirectly through the outer method)

Prometheus metrics

Metric Type Labels Description
coverage_method_info Gauge (always 1) method_id, class, method, file, line Metadata record — value is always 1, all useful data is in the labels
coverage_method_hit Gauge (0 or 1) method_id 1 if the method has been called at least once since the process started, 0 otherwise
coverage_method_calls_total Counter method_id Monotonically increasing call count since startup

method_id is an FNV-32 hash of the method's full CLR name (e.g., MyApp.Services.OrderService::PlaceOrder(System.Int32,System.Decimal)). It is stable across compilations as long as the method name and parameter types do not change, which makes it safe to use as a long-lived Prometheus label and to join across the info metric.

The coverage_method_info metric follows the Prometheus info metric pattern (the same pattern used by kube_pod_info, go_build_info, etc.): a gauge always equal to 1 whose labels carry structured metadata. This lets PromQL joins (group_left) attach class, file, and line information to the hit and call metrics without repeating those strings on every scrape of the high-frequency metrics.

Useful PromQL queries

Overall coverage rate (percentage of methods hit at least once):

count(coverage_method_hit == 1) / count(coverage_method_hit) * 100

Methods with full metadata — the foundation of the Grafana panel:

coverage_method_hit * on(method_id) group_left(class, method, file, line)
  coverage_method_info

Call counts with full metadata — for the "hot path" view:

coverage_method_calls_total * on(method_id) group_left(class, method, file, line)
  coverage_method_info

Top 20 most called methods:

topk(20, coverage_method_calls_total * on(method_id) group_left(class, method, file, line)
  coverage_method_info)

Methods never called (dead code candidates):

coverage_method_hit == 0

Coverage rate per source file:

sum by(file) (coverage_method_hit * on(method_id) group_left(file) coverage_method_info)
  /
sum by(file) (coverage_method_info)
* 100

Grafana panel

Install the BlazorAmpowering Grafana panel plugin and configure two Prometheus queries on the panel:

Ref ID Query
A coverage_method_hit * on(method_id) group_left(class,method,file,line) coverage_method_info
B coverage_method_calls_total * on(method_id) group_left(class,method,file,line) coverage_method_info

The panel renders a collapsible file → class → method tree with:

  • A green ✓ / red ✕ hit badge per method
  • Call count or "never called" label
  • Per-file and per-class coverage percentages
  • A global coverage bar and filter input at the top

Performance

The injected prologue consists of exactly 4 IL instructions. On a modern x64 CPU:

Operation Cost
ldsfld s_baseSlot ~1 ns (static field, likely L1 cached)
ldc.i4 + add < 1 ns (register arithmetic)
CoverageRegistry.Hit() bounds check ~1 ns
Volatile.Write (first hit only, branch-predicted) ~1 ns amortized
Interlocked.Increment ~1–2 ns (cache line contention under load)

Total per-call overhead: ~4–5 ns. A method called 1 million times per second adds roughly 4–5 ms/s of CPU time — less than 1 % on any non-trivial workload. The Volatile.Write for the hit flag is skipped on subsequent calls by a conditional branch that is perfectly predicted after the first invocation.


License

Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 86 5/17/2026