Extends Verify to allow snapshot testing and structural validation of Mono.Cecil modules, types and members. An alternative to PEVerify.
See Milestones for release notes.
Entity Framework Extensions is a major sponsor and is proud to contribute to the development this project.
There are two packages, built from the same source. Use the one that matches the Mono.Cecil already in the test project.
- Verify.Cecil targets the Mono.Cecil package.
- Verify.Cecil.FodyHelpers targets the Mono.Cecil bundled in FodyHelpers. Use this for Fody weaver tests.
The Mono.Cecil bundled in FodyHelpers is signed with a different key to the Mono.Cecil package, so the two cannot be mixed. A test project that references FodyHelpers should also avoid the TUnit metapackage, since it brings in the Mono.Cecil package. Reference TUnit.Core, TUnit.Engine and TUnit.Assertions instead.
[ModuleInitializer]
public static void Init() =>
VerifyCecil.Initialize();The following Cecil types can be verified: AssemblyDefinition, ModuleDefinition, TypeDefinition, MethodDefinition, FieldDefinition, PropertyDefinition and EventDefinition.
The output is ILAsm-like text. It is deterministic: RVAs, metadata tokens, MVIDs and the source revision in AssemblyInformationalVersionAttribute are excluded, and instruction offsets are computed from the instructions. So a method modified in memory is shown with the offsets it will have when written.
Given the following type:
public class Target :
INotifyPropertyChanged
{
void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new(propertyName));
public event PropertyChangedEventHandler? PropertyChanged;
string? property;
public string? Property
{
get => property;
set
{
property = value;
OnPropertyChanged();
}
}
}[Test]
public async Task TypeUsage()
{
using var module = ModuleDefinition.ReadModule(assemblyPath);
await Verify(module.GetType("Target"));
}Result:
.class public auto ansi beforefieldinit Target
extends [System.Runtime]System.Object
implements [System.ObjectModel]System.ComponentModel.INotifyPropertyChanged
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (2)
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (0)
.interfaceimpl type [System.ObjectModel]System.ComponentModel.INotifyPropertyChanged
.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = (0)
.field private class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler PropertyChanged
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
.custom instance void [System.Runtime]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggerBrowsableState) = ([System.Runtime]System.Diagnostics.DebuggerBrowsableState(0))
.field private string property
.method private hidebysig instance void OnPropertyChanged([opt] string propertyName) cil managed
{
.param [1] = nullref
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CallerMemberNameAttribute::.ctor()
IL_0000: ldarg.0
IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
IL_0006: dup
IL_0007: brtrue.s IL_000c
IL_0009: pop
IL_000a: br.s IL_0019
IL_000c: ldarg.0
IL_000d: ldarg.1
IL_000e: newobj instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs::.ctor(string)
IL_0013: callvirt instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler::Invoke(object, class [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs)
IL_0018: nop
IL_0019: ret
}
.method public hidebysig newslot specialname virtual final instance void add_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler value) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
.locals init (
[0] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
[1] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
[2] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
)
IL_0000: ldarg.0
IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: ldarg.1
IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Combine(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate)
IL_0010: castclass [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
IL_0015: stloc.2
IL_0016: ldarg.0
IL_0017: ldflda class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
IL_001c: ldloc.2
IL_001d: ldloc.1
IL_001e: call !!0 [System.Threading]System.Threading.Interlocked::CompareExchange<class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler>(!!0&, !!0, !!0)
IL_0023: stloc.0
IL_0024: ldloc.0
IL_0025: ldloc.1
IL_0026: bne.un.s IL_0007
IL_0028: ret
}
.method public hidebysig newslot specialname virtual final instance void remove_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler value) cil managed
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
.locals init (
[0] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
[1] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler,
[2] class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
)
IL_0000: ldarg.0
IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: ldarg.1
IL_000b: call class [System.Runtime]System.Delegate [System.Runtime]System.Delegate::Remove(class [System.Runtime]System.Delegate, class [System.Runtime]System.Delegate)
IL_0010: castclass [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler
IL_0015: stloc.2
IL_0016: ldarg.0
IL_0017: ldflda class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
IL_001c: ldloc.2
IL_001d: ldloc.1
IL_001e: call !!0 [System.Threading]System.Threading.Interlocked::CompareExchange<class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler>(!!0&, !!0, !!0)
IL_0023: stloc.0
IL_0024: ldloc.0
IL_0025: ldloc.1
IL_0026: bne.un.s IL_0007
IL_0028: ret
}
.method public hidebysig specialname instance string get_Property() cil managed
{
IL_0000: ldarg.0
IL_0001: ldfld string Target::property
IL_0006: ret
}
.method public hidebysig specialname instance void set_Property(string value) cil managed
{
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: stfld string Target::property
IL_0008: ldarg.0
IL_0009: ldstr "Property"
IL_000e: call instance void Target::OnPropertyChanged(string)
IL_0013: nop
IL_0014: ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
}
.property instance string Property()
{
.get instance string Target::get_Property()
.set instance void Target::set_Property(string)
}
.event [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler PropertyChanged
{
.addon instance void Target::add_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler)
.removeon instance void Target::remove_PropertyChanged(class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler)
}
}[Test]
public async Task MethodUsage()
{
using var module = ModuleDefinition.ReadModule(assemblyPath);
var type = module.GetType("Target");
await Verify(type.Methods.Single(_ => _.Name == "OnPropertyChanged"));
}Result:
.method private hidebysig instance void OnPropertyChanged([opt] string propertyName) cil managed
{
.param [1] = nullref
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CallerMemberNameAttribute::.ctor()
IL_0000: ldarg.0
IL_0001: ldfld class [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler Target::PropertyChanged
IL_0006: dup
IL_0007: brtrue.s IL_000c
IL_0009: pop
IL_000a: br.s IL_0019
IL_000c: ldarg.0
IL_000d: ldarg.1
IL_000e: newobj instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs::.ctor(string)
IL_0013: callvirt instance void [System.ObjectModel]System.ComponentModel.PropertyChangedEventHandler::Invoke(object, class [System.ObjectModel]System.ComponentModel.PropertyChangedEventArgs)
IL_0018: nop
IL_0019: ret
}[Test]
public async Task PropertyUsage()
{
using var module = ModuleDefinition.ReadModule(assemblyPath);
var type = module.GetType("Target");
await Verify(type.Properties.Single(_ => _.Name == "Property"));
}Result:
.property instance string Property()
{
.get instance string Target::get_Property()
.set instance void Target::set_Property(string)
}[Test]
public async Task ModuleUsage()
{
using var module = ModuleDefinition.ReadModule(assemblyPath);
await Verify(module);
}A snapshot detects changes, but not IL that is invalid. So before converting, the IL is checked, and verification fails with a CecilValidationException listing the problems. This covers the problems PEVerify commonly reports for woven assemblies:
- Branch and switch targets that are not in the method body.
- Short branches whose target is out of range. Cecil silently writes a wrong offset for these.
- Stack underflow, differing stack depths where paths merge, items left on the stack at
ret, and a non-empty stack on entry to atry. - Control falling through the end of the method body, and empty method bodies.
- Exception handlers with boundaries missing or outside the body, empty or overlapping ranges, and catch handlers without a catch type.
- Operands of the wrong type, arguments and locals that do not exist, and variables or parameters from another method.
- Method, field and type references that do not resolve, and references that do not match the definition: static vs instance fields and methods,
newobjon a method that is not a constructor, andcallvirton a static method. - Abstract methods in types that are not abstract.
References are only checked when the assembly containing them can be found. If it cannot, the check is skipped rather than reported.
[Test]
public async Task ValidationThrows()
{
using var module = ModuleDefinition.ReadModule(assemblyPath);
var method = AddMethod(
module,
module.TypeSystem.Void,
il =>
{
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ret);
});
await Assert.ThrowsAsync<CecilValidationException>(
async () => await Verify(method));
}To snapshot without validating:
[Test]
public async Task DisableCecilValidation()
{
using var module = ModuleDefinition.ReadModule(assemblyPath);
var method = AddMethod(
module,
module.TypeSystem.Void,
il =>
{
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Ret);
});
await Verify(method)
.DisableCecilValidation();
}CecilValidator can also be used directly:
IReadOnlyList<string> problems = CecilValidator.Validate(module);
CecilValidator.ThrowIfInvalid(module);PEVerify only runs on .NET Framework and needs the Windows SDK installed. Verify.Cecil.FodyHelpers runs on any platform and target framework.
Disable PEVerify in ExecuteTestRun and verify the woven types:
[Test]
public async Task WeaverUsage()
{
var weaver = new ModuleWeaver();
var result = weaver.ExecuteTestRun(
"AssemblyToProcess.dll",
// Verify.Cecil replaces PEVerify
runPeVerify: false);
using var module = ModuleDefinition.ReadModule(result.AssemblyPath);
await Verify(module.GetType("Target"));
}A weaver that produces invalid IL fails the test:
[Test]
public async Task BrokenWeaver()
{
var weaver = new ModuleWeaver
{
Broken = true
};
var result = weaver.ExecuteTestRun(
"AssemblyToProcess.dll",
runPeVerify: false,
assemblyName: "BrokenWeaver");
using var module = ModuleDefinition.ReadModule(result.AssemblyPath);
var exception = await Assert.ThrowsAsync<CecilValidationException>(
async () => await Verify(module.GetType("Target")));
await Assert.That(exception!.Problems)
.IsEquivalentTo(
[
"System.String Target::Injected(): " +
"IL_0006: ret: stack must be empty after ret, but has 1 left"
]);
}Helmet designed by Leonidas Ikonomou from The Noun Project.

