Migration Playbook
A practical guide to planning and executing library migrations with WrapGod.
Table of contents
- Choosing a migration strategy
- Authoring mappings
- Running the pipeline
- Safety model
- Version divergence handling
- Validation checklist
- Troubleshooting FAQ
Choosing a migration strategy
WrapGod supports three compatibility modes. Choose the one that matches your migration scenario:
LCD (Lowest Common Denominator)
Generate wrappers that work across all extracted versions. Only members present in every version appear in the wrapper interface.
When to use: You maintain a library consumed by projects targeting different versions of a dependency, and you need a single wrapper that works everywhere.
Targeted
Generate wrappers for a single specific version. The wrapper surface matches that version exactly.
When to use: You are migrating from one library version to another (or to a different library entirely) and only need to support the target version.
Adaptive
Generate wrappers with runtime version guards. Members present in some but not all versions are conditionally available, with the facade checking at runtime which version is loaded.
When to use: You need maximum API coverage while supporting multiple versions simultaneously during a gradual rollout.
Authoring mappings
WrapGod supports three configuration surfaces for defining how source types and members map to target equivalents. They can be combined with configurable merge precedence.
JSON configuration files
Create a *.wrapgod.config.json file:
{
"migrationName": "LibA-to-LibB",
"sourceLibrary": "LibA",
"targetLibrary": "LibB",
"mappings": [
{
"source": ".MethodA({args})",
"target": ".MethodB({args})",
"safety": "safe"
}
]
}
Strengths: Portable, version-controllable, works without code changes.
C# attributes
Annotate types and members with [WrapType] and [WrapMember]:
[WrapType(typeof(SourceLib.Client))]
public interface IWrappedClient
{
[WrapMember(nameof(SourceLib.Client.Send))]
void Transmit(string data);
}
Strengths: Co-located with code, compiler-verified, refactoring-safe.
Fluent DSL
Use the fluent configuration API:
WrapGodConfig.For<SourceLib.Client>()
.MapMember(c => c.Send, "Transmit")
.WithSafety(Safety.Safe);
Strengths: Strongly typed, IDE-friendly, composable.
Merge precedence
When multiple surfaces define mappings for the same member, precedence determines which wins (default order):
- Attributes (highest)
- Fluent DSL
- JSON config (lowest)
Override in config: "mergePrecedence": ["json", "fluent", "attributes"]
Running the pipeline
A typical WrapGod migration follows four stages:
1. Extract
Produce a manifest from the source assembly:
wrap-god extract path/to/Source.dll --output manifest.wrapgod.json
Or programmatically:
var manifest = AssemblyExtractor.Extract("path/to/Source.dll");
The manifest captures every public type and member with stable identifiers.
2. Configure
Author your mapping rules (JSON, attributes, or fluent DSL) defining how source APIs translate to target APIs. Mark each mapping with a safety level.
3. Generate
Add the manifest and WrapGod.Generator to your project:
<ItemGroup>
<AdditionalFiles Include="manifest.wrapgod.json" />
</ItemGroup>
Build to generate wrapper interfaces and facade classes automatically via the Roslyn incremental source generator.
4. Analyze and fix
Add WrapGod.Analyzers to detect direct usage of wrapped types:
- WG2001: Direct use of a wrapped type (should use
IWrapped{Type}) - WG2002: Direct call to a wrapped member (should use facade)
Code fixes are available for safe mappings. Guided mappings produce warnings with suggested rewrites.
Safety model
Every mapping carries a safety classification that determines how WrapGod handles it:
Safe (auto-fix)
Direct 1:1 replacements where the semantics are identical. WrapGod applies these automatically via code fixes.
Examples:
It.IsAny<T>()->Arg.Any<T>().Should().BeTrue()->.ShouldBeTrue()
Guided (semi-automatic)
Structural rewrites where the transformation is well-defined but requires verification. WrapGod flags these as warnings and suggests the rewrite.
Examples:
mock.Setup(x => x.Foo()).Returns(bar)->sub.Foo().Returns(bar)act.Should().Throw<T>()->Should.Throw<T>(act)
Manual (human required)
Patterns with no safe automated equivalent. WrapGod reports a diagnostic but does not offer a code fix.
Examples:
mock.VerifyAll()(no NSubstitute equivalent)Arg.Do<T>()(no Moq equivalent)
Version divergence handling
When migrating between versions of the same library (or libraries that have evolved), WrapGod provides tools for managing API differences:
Multi-version extraction
var manifests = new[]
{
AssemblyExtractor.Extract("Source.v1.dll"),
AssemblyExtractor.Extract("Source.v2.dll"),
};
var merged = MultiVersionExtractor.Merge(manifests);
The merged manifest annotates each type and member with version presence
metadata (IntroducedIn, RemovedIn, ChangedIn).
Breaking change detection
Members that exist in one version but not another are flagged. The compatibility mode you choose determines how these are handled:
- LCD: Omitted from the wrapper (safe but limited)
- Targeted: Present based on the chosen version
- Adaptive: Wrapped with runtime guards
Migration config versioning
Pin your config to specific source/target versions:
{
"sourceVersion": "4.20.72",
"targetVersion": "5.3.0",
"mappings": [...]
}
Validation checklist
Before considering a migration complete:
- [ ] All example projects build (
dotnet build) - [ ] All tests pass (
dotnet test) - [ ] No WG2001/WG2002 diagnostics remain (or are explicitly suppressed)
- [ ] All
guidedmappings have been reviewed by a human - [ ] All
manualmappings have been addressed with handwritten code - [ ] Config matches the exact source and target library versions in use
- [ ] README or migration notes document any behavioral differences
- [ ] CI pipeline validates the migrated projects build and test successfully
- [ ] Integration or smoke tests exercise the migrated code paths
- [ ] Rollback plan documented (keep both dependencies until verified)
Troubleshooting FAQ
The extractor fails with "Assembly file not found"
Ensure you are pointing to the compiled DLL, not the NuGet package or project file. The assembly must exist on disk. Build the project first if needed.
The generator does not emit any files
Check that:
- The manifest file is included as
<AdditionalFiles>(not<None>or<Content>) - The manifest file extension is
.wrapgod.json - WrapGod.Generator is referenced as an analyzer: the
.csprojshould includeOutputItemType="Analyzer"on the project reference
Analyzer reports WG2001 but I want to use the original type
Suppress the diagnostic for intentional direct usage:
#pragma warning disable WG2001
var client = new OriginalClient();
#pragma warning restore WG2001
Or in .editorconfig:
[*.cs]
dotnet_diagnostic.WG2001.severity = none
Config mappings are not being applied
Check merge precedence. If attributes define a mapping for the same member,
they take priority over JSON config by default. Either remove the attribute
or adjust mergePrecedence in your config.
Guided mapping produces unexpected code
Guided mappings are suggestions, not guaranteed-correct transformations. Always
review the generated code. If the suggestion is wrong, mark the mapping as
manual and write the replacement by hand.
Build succeeds but tests fail after migration
Common causes:
- Behavioral differences between source and target libraries (e.g., Moq strict mode vs NSubstitute's default leniency)
- Argument matcher semantics that differ subtly between frameworks
- Exception types that changed between libraries
- Ordering guarantees that one library provides but the other does not
Review the notes field in your migration config for known behavioral
differences.