BlazorAmpowering.Coverage
1.0.0
dotnet add package BlazorAmpowering.Coverage --version 1.0.0
NuGet\Install-Package BlazorAmpowering.Coverage -Version 1.0.0
<PackageReference Include="BlazorAmpowering.Coverage" Version="1.0.0" />
<PackageVersion Include="BlazorAmpowering.Coverage" Version="1.0.0" />
<PackageReference Include="BlazorAmpowering.Coverage" />
paket add BlazorAmpowering.Coverage --version 1.0.0
#r "nuget: BlazorAmpowering.Coverage, 1.0.0"
#:package BlazorAmpowering.Coverage@1.0.0
#addin nuget:?package=BlazorAmpowering.Coverage&version=1.0.0
#tool nuget:?package=BlazorAmpowering.Coverage&version=1.0.0
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:
- Scan every method in the assembly.
- 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. - 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
- Generate a CLR module initializer (
[ModuleInitializer]) that runs automatically when the assembly is first loaded by the CLR. This initializer callsCoverageRegistry.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 everyasyncmethod - 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 | Versions 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. |
-
.NETStandard 2.0
- Fody (>= 6.9.1)
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 |