Mihadeth.LegacyWrapper
1.0.0-preview.3
dotnet tool install --global Mihadeth.LegacyWrapper --version 1.0.0-preview.3
dotnet new tool-manifest
dotnet tool install --local Mihadeth.LegacyWrapper --version 1.0.0-preview.3
#tool dotnet:?package=Mihadeth.LegacyWrapper&version=1.0.0-preview.3&prerelease
nuke :add-package Mihadeth.LegacyWrapper --version 1.0.0-preview.3
Legacy Wrapper
A .NET CLI tool that prepares a legacy .NET Framework application for cloud deployment without modifying a single file of the original codebase. It reads your existing solution, produces a detailed compatibility report, packages your already-compiled binaries as NuGet packages declared as netstandard2.0, and scaffolds a modern .NET 10 host project that references them.
This is the Strangler Fig pattern applied to .NET migration. The legacy solution keeps running in production untouched; a new, independent wrapper solution is generated alongside it, and you migrate internals incrementally afterwards — one package at a time — instead of doing a big-bang rewrite.
The technical premise: .NET Framework 4.6.1 and later implement .NET Standard 2.0, and so does modern .NET. Framework-compiled assemblies declared as netstandard2.0 in a .nuspec load on .NET 10 through the compatibility shim.
Status: preview (1.0.0-preview.2). The command surface, defaults, and diagnostic IDs described here may change before 1.0.0. This page documents 1.0.0-preview.2 specifically.
MIT licensed. Questions, bugs, or help wrapping a specific solution: mihadeth@gmail.com (see Support at the end of this page).
⚠️ Important — what you get, and what you still do.
Automated: analysis, compatibility reporting, NuGet packaging of your compiled binaries, and a complete .NET 10 host scaffold — project file, DI wiring, configuration bridge, Dockerfile.
Manual — HTTP endpoints. Web API controllers, IIS handlers, and REST-style WCF are generated with correct route templates and HTTP-method attributes, but the parameter signatures are stubbed — route tokens are emitted as
stringregardless of the legacy parameter's real type, request bodies as a single[FromBody] object?, and query-string parameters not at all. The action bodies throwNotImplementedExceptionuntil you retype the signature and wire each action to your business-logic classes. This is structural: legacyApiControllerandIHttpHandlertypes derive fromSystem.Webtypes that cannot load on .NET 10, so the generated proxy cannot instantiate them. Budget roughly one edit per endpoint.Manual — connection strings and detected secrets. Replaced with placeholders rather than copied. The wrapper connects to nothing until you supply real values.
The exception: WCF services hosted through CoreWCF register and invoke your legacy implementations directly, with no manual step.
On this page: Who this is for · When not to use it · Requirements and prerequisites (start with the .NET Framework 4.6.1 floor) · Install · Quick start · Command reference · Understanding the compatibility report · What gets generated · Remediation and IL rewriting · Troubleshooting · Limitations · Support
New here? Read Requirements and prerequisites, then run legacy-wrapper analyze — it is read-only, needs no legacy build tools, and tells you within minutes whether the rest of this page applies to you.
Who this is for
Use Legacy Wrapper when you have:
- A .NET Framework 4.6.1 or later solution (4.7.2+ recommended) whose value is in business-logic assemblies — services, domain models, data access, calculation engines.
- WCF services, especially ones using BasicHttpBinding, NetTcpBinding, WSHttpBinding, or NetNamedPipeBinding. These get the most automation, because CoreWCF hosting invokes your real types.
- Web API or MVC controllers whose logic you are willing to re-point at the underlying services during a controlled wire-up pass.
- A mandate to containerize or move to the cloud without a code-freeze rewrite, and a need to prove feasibility with evidence before committing budget.
Legacy Wrapper is also useful purely as an assessment tool. The analyze command is read-only, requires no legacy build tools, needs no compiled binaries, and produces a self-contained HTML report you can circulate.
When not to use it
| Situation | Why |
|---|---|
The application is ASP.NET Web Forms (.aspx, .ascx, .master) |
Web Forms has no .NET Core equivalent. The tool detects it and reports WEB-FORMS-001 as Blocking. It does not wrap it. Non-UI business logic in the same solution can still be wrapped. |
The application exposes ASMX web services (.asmx) |
ASMX cannot be hosted on .NET Core. Reported as ASMX-001, Blocking. Each operation needs converting to an ASP.NET Core controller action or a CoreWCF service. |
| Any project targets below .NET Framework 4.6.1 | The netstandard2.0 packaging premise is invalid below 4.6.1. Hard-blocked — see the next section. |
Core messaging runs on MSMQ (netMsmqBinding) or duplex HTTP (wsDualHttpBinding) or WS-Federation |
No CoreWCF equivalent. Reported WCF-B01, WCF-B02, WCF-B03 — all Blocking. |
| You want an actual port to modern .NET | This tool deliberately does not migrate your code. If your goal is genuine modernization of the source, use an upgrade-oriented tool instead. |
| The value is almost entirely in HTTP endpoints with thin or no service layer | You will get analysis plus routing scaffolding, but the wire-up work per endpoint may approach the cost of rewriting the controller. |
| The solution contains VB.NET or F# projects whose logic matters | Source analysis is C# only. .vbproj and .fsproj projects are still packaged and still binary-analyzed with Mono.Cecil, but the eight source analyzers, endpoint discovery, WCF discovery, and IoC discovery never run on them — and no diagnostic tells you so. A VB.NET project can report zero findings simply because nothing looked at it. Treat any VB/F# project's score as uninformative and review it by hand. |
Requirements and prerequisites
The .NET Framework 4.6.1 floor — read this first
Every project in your solution must target .NET Framework 4.6.1 or later. 4.6.1 is the first version that implements .NET Standard 2.0; binaries built against anything older cannot be loaded through the compatibility shim, so packaging them is pointless. This is enforced, not merely advised.
| Target framework | Result |
|---|---|
Anything below 4.6.1 — v2.0, v3.5, v4.0, v4.5, v4.5.2, v4.6, and the equivalent net20 … net46 monikers |
Blocked. TFM-001 Blocking diagnostic; package and wrap exit with code 4. |
v4.6.1–v4.8.1, net461–net481 |
Supported. 4.6.1 through 4.7.1 work but implement parts of .NET Standard 2.0 through facade assemblies; 4.7.2 or 4.8 is recommended. |
netstandard*, netcoreapp*, net5.0 and later |
Already modern. No diagnostic. |
| Unparseable or missing | Treated as unknown. Not blocking, but support is not claimed. |
The fix is a retarget, not a migration: change <TargetFrameworkVersion> to v4.7.2 (or v4.8), or <TargetFramework> to net472/net48 for SDK-style projects, rebuild the legacy solution, and rerun the tool. No source changes are implied by TFM-001 itself. This converts a hard "no" into roughly a day of work, which is why it is worth checking before anything else.
Run legacy-wrapper analyze first — it reports TFM-001 per offending project and still writes a full report, so you learn about every below-floor project in one pass.
To run the tool
- .NET 10 SDK — Legacy Wrapper is a .NET 10 global tool. The runtime alone is not enough: Roslyn source analysis loads your projects through MSBuild, which requires the SDK. With only the runtime installed, every project degrades to a
SRC-*warning and you get an "analysis incomplete" report with no real findings. Download: https://dotnet.microsoft.com/download/dotnet/10.0 - Your legacy solution should be NuGet-restored before you analyze it (
nuget restore Legacy.sln). Without restored packages, Roslyn frequently loads projects with zero documents, which producesSRC-LOADwarnings and an unreliable score. - Windows, macOS, or Linux. Analysis works identically on all three.
To build your legacy solution
This is the part that is genuinely platform-dependent, and the honest answer is not "it works everywhere".
| Command | Needs a legacy build tool? | Needs compiled legacy binaries? | Needs network? |
|---|---|---|---|
doctor |
No — that is what it checks | No | No |
analyze |
No (the .NET 10 SDK is still required — see above) | Optional. With binaries present, additional binary-level analysis runs (BIN-REF-*, PS-BIN, PS-PINVOKE) |
Optional. nuget.org is queried for third-party package compatibility; on failure those packages are scored Unknown rather than failing the run |
package |
Yes, unless --skip-build |
Yes, or projects are skipped | Only if nuget.exe is absent and the dotnet pack fallback must restore |
wrap |
Yes, unless --skip-build |
Yes | As above |
generate |
No | No — but packages must already exist at --packages |
No |
Classic non-SDK .NET Framework .csproj files need a real MSBuild. The tool probes for MSBuild first (via vswhere and PATH on Windows, and msbuild on PATH elsewhere), then falls back to dotnet build, which works for SDK-style projects and is limited for legacy non-SDK ones. Mono's xbuild is deliberately not accepted — it was retired in 2019 and cannot build modern .NET Framework solutions.
Practical guidance:
On Windows, install Visual Studio 2022 Build Tools with the managed-desktop workload — the bare install ships no workloads and leaves MSBuild missing:
winget install Microsoft.VisualStudio.2022.BuildTools --override "--quiet --add Microsoft.VisualStudio.Workload.ManagedDesktopBuildTools --add Microsoft.VisualStudio.Workload.NetCoreBuildTools --add Microsoft.Net.Component.4.8.SDK --add Microsoft.Net.Component.4.7.2.TargetingPack"Add the targeting pack matching your solution's framework version. Confirm with
legacy-wrapper doctor, which must show an MSBuild row before you rely on it. This is the smoothest path.On macOS or Linux, the recommended workflow is to build the legacy solution on Windows or in CI, copy the
binoutput, and runpackageorwrapwith--skip-build. Alternatively install Mono'smsbuild(brew install mono/sudo apt install mono-devel) and expect mixed results with older project types.legacy-wrapper analyzeneeds no legacy build tool at all and is fully usable cross-platform.
nuget.exe is optional. When it is missing, the tool generates a minimal SDK-style project referencing the .nuspec and packs with dotnet pack instead. doctor shows this as a warning row, never a failure.
What leaves your machine
Analysis, packaging, and generation run entirely locally. Your source, binaries, and configuration are never uploaded. Two exceptions, both visible and both optional:
- nuget.org queries.
analyze,generate, andwrapsend the package IDs and versions referenced by your solution tohttps://api.nuget.org/v3-flatcontainer/to check modern compatibility. No source, no assembly, no configuration. With no network access these packages are scored Unknown and the run continues. --ai-provider. Only when you explicitly pass it. Code context for the affected types is sent to Anthropic or OpenAI. Off by default — see the AI section below.
The tool writes nothing outside your --output/--report paths and a temporary directory it deletes afterwards. It never modifies your legacy solution.
Install
dotnet tool install -g Mihadeth.LegacyWrapper --prerelease
The --prerelease flag is required while the tool is in preview — without it, dotnet tool install finds no matching version.
The installed command is legacy-wrapper, not the package name:
legacy-wrapper --version
legacy-wrapper --help
To update or remove:
dotnet tool update -g Mihadeth.LegacyWrapper --prerelease
dotnet tool uninstall -g Mihadeth.LegacyWrapper
If legacy-wrapper is not found after installing, ~/.dotnet/tools (macOS/Linux) or %USERPROFILE%\.dotnet\tools (Windows) is not on your PATH.
Run doctor first
legacy-wrapper doctor
Example output:
Legacy Wrapper — Prerequisites Check
═══════════════════════════════════════
✅ .NET SDK 10.0.108 (dotnet)
❌ MSBuild not found
✅ nuget.exe found (/opt/homebrew/bin/nuget)
All required tools are available.
MSBuild was not found — .NET Framework (non-SDK) solutions may not build:
→ Building .NET Framework projects requires Windows MSBuild or Mono's 'msbuild' command.
→ Install Mono msbuild: brew install mono
→ Or use --skip-build with binaries compiled elsewhere.
⚠️ Warning — a green
doctoris not a green build.doctorexits 0 when the .NET SDK alone is present, even with MSBuild missing — the summary line reads "All required tools are available." For classic non-SDK .NET Framework solutions, the MSBuild caveat block underneath is the signal that matters. A greendoctoron macOS or Linux does not guarantee your legacy solution will build.
doctor exits 1 when neither the .NET SDK nor MSBuild is found, and prints platform-specific install commands.
Quick start
Step 1 — Analyze (read-only, safe, no legacy build required)
legacy-wrapper analyze --solution ./Legacy.sln --format all --report ./reports/Legacy
Writes reports/Legacy.html, reports/Legacy.json, and reports/Legacy.md, and prints a summary: overall score, per-project breakdown, up to 15 top issues ordered by severity, and discovered entrypoints (Web API/MVC endpoints, HTTP handlers, WCF services, IoC containers).
Read the HTML report before spending time on a build. If Blocking: is non-zero, wrap will very likely stop at exit code 4.
ℹ️ Note.
analyzeexits 0 regardless of the score — even at 0/100 with blockers present. Do not use its exit code as a go/no-go gate; read the report or the JSON output instead.
Step 2 — See which issues have a bridge available
legacy-wrapper analyze --solution ./Legacy.sln --show-remediable
Adds a === Remediation Summary === block: how many diagnostics have bridges available, the name and target file of each bridge class, any license note attached to one, how many extra NuGet packages the bridges pull in, and how many DI registrations are flagged for review.
The HTML report labels this same count IL-Rewritable; the console labels it Bridges available. They are the same set.
Its closing line says "Run with --remediate". That means re-run generate or wrap with --remediate — analyze itself does not accept that flag.
Step 3 — Run the full pipeline
legacy-wrapper wrap --solution ./Legacy.sln --output ./GeneratedWrapper
Three labelled steps (Analyze, Package, Generate) followed by a summary. Produces ./GeneratedWrapper/ containing WrapperHost.sln, src/WrapperHost/, local-packages/, nuget.config, Dockerfile, docker-compose.yml, and COMPATIBILITY-REPORT.html.
If it exits 4, unremediable blocking issues were found. The list of [ID] Title items goes to standard error, only COMPATIBILITY-REPORT.html was written, and no wrapper was generated. That report is your work list.
Step 4 — Build and run the wrapper
cd GeneratedWrapper
dotnet build WrapperHost.sln
docker compose up --build
The generated Dockerfile is a multi-stage build on the .NET 10 SDK and ASP.NET runtime Linux images and exposes port 8080; docker-compose.yml maps 8080:8080.
The generated solution is designed to build without edits, and the generator is tested against compiled output. Real legacy solutions vary: if dotnet restore fails with NU1101 or NU1202, a legacy project was skipped during packaging or one of its dependencies has no .NET Standard-compatible version — check the Packages: N created and Skipped: N project(s) lines from the packaging step first. See the What gets generated section below for which endpoints actually execute before you wire them up.
Common variations
# Binaries already built elsewhere (for example a Windows CI drop).
# Also skips the build-tool pre-flight check entirely.
legacy-wrapper wrap -s ./Legacy.sln -o ./GeneratedWrapper --skip-build
# Use Debug binaries instead of Release
legacy-wrapper wrap -s ./Legacy.sln -o ./GeneratedWrapper -c Debug
# Large solution: bundle everything into one aggregate package above 5 projects
legacy-wrapper wrap -s ./Legacy.sln -o ./GeneratedWrapper -m 5
# Always one package per project, no matter how many
legacy-wrapper wrap -s ./Legacy.sln -o ./GeneratedWrapper -m 0
# Rewrite IL call sites to route through the generated bridges
legacy-wrapper wrap -s ./Legacy.sln -o ./GeneratedWrapper --rewrite-il
# Generate DI adapter classes (TODO stubs unless an AI provider is configured)
legacy-wrapper wrap -s ./Legacy.sln -o ./GeneratedWrapper --di-overrides
Split pipeline
Use the individual commands when you need the stages to run in different places — for example, packaging on a Windows build agent and generating on a developer machine.
legacy-wrapper package --solution ./Legacy.sln --output ./local-packages
legacy-wrapper generate --solution ./Legacy.sln --packages ./local-packages --output ./Wrapper --remediate
⚠️ Warning — two rules apply to the split pipeline.
- Use the same
--max-projectsvalue on both commands. They independently decide per-project versus aggregate packaging. If they disagree, the generated.csprojreferences packages that do not exist in the feed.- If you used
package --rewrite-il,generatemust include--remediate. IL rewriting retargets call sites into bridge classes in theWrapperHost.Remediationnamespace, and those classes only exist when remediation code is generated. Without--remediatethe wrapper compiles but throwsTypeLoadExceptionorMissingMethodExceptionthe first time a rewritten call executes.wrap --rewrite-ilis not affected —wrapalways generates bridges.
Command reference
Five commands: analyze, package, generate, wrap, doctor. No command has an alias. --solution uses the short alias -s everywhere; all other short aliases are per-command.
Global options
Available on every command.
| Option | Alias | Default | Description |
|---|---|---|---|
--verbose |
-v |
off | Print full exception details (type, stack trace, inner exceptions) on failure |
--log-file <path> |
— | none | Write a copy of all output, including full exception details, to the given file |
--verbose affects failure output only — it does not make progress output chattier. There is no --quiet; progress output is unconditional.
--log-file mirrors both standard output and standard error to the file, overwriting it each run. Importantly, the full exception detail is written to the log file even without --verbose, so --log-file alone gives you a terse console and a complete diagnostic record. This is the recommended combination when reporting a problem.
legacy-wrapper analyze
Analyze a legacy .NET Framework solution for compatibility. Read-only. Runs no build, needs no legacy build tools, writes nothing except report files.
| Option | Alias | Type | Default | Required |
|---|---|---|---|---|
--solution |
-s |
file | — | Yes |
--report |
-r |
file path | COMPATIBILITY-REPORT in the current directory |
No |
--format |
-f |
string | html |
No |
--show-remediable |
— | flag | False |
No |
--format accepts html, json, md, or all. all expands to exactly html, json, md.
ℹ️ Note — the extension you write in
--reportis discarded and replaced by the format. The directory and the base filename are taken from--report; the extension always comes from--format. So--report out/foo.html --format jsonwritesout/foo.json, and--report out/x.html --format allwritesout/x.html,out/x.json, andout/x.md. With no--reportat all and the default--format html, the output is./COMPATIBILITY-REPORT.html.
⚠️ Warning — an invalid
--formatvalue is not an error. It runs the entire analysis, writes nothing, printsUnknown format: {value}to standard error, and still exits 0. Check for theHTML report: …/JSON report: …/Markdown report: …confirmation lines.
Exit codes: 0 on success (including a 0/100 score, and including an unknown format); 1 if the solution file does not exist or analysis throws. analyze never returns 2, 3, or 4.
legacy-wrapper package
Build the legacy solution and package the compiled binaries as NuGet packages.
| Option | Alias | Type | Default | Required |
|---|---|---|---|---|
--solution |
-s |
file | — | Yes |
--output |
-o |
directory | ./local-packages |
No |
--skip-build |
— | flag | False |
No |
--configuration |
-c |
string | Release |
No |
--rewrite-il |
— | flag | False |
No |
--max-projects |
-m |
integer | 10 |
No |
Behavior notes:
- A build-tool pre-flight check runs first, before any other work, unless
--skip-buildis passed. If neither the .NET SDK nor MSBuild is found, the fulldoctorreport is written to standard error and the command exits 2 immediately. - A .NET Framework 4.6.1 floor gate runs before the build. Any project below the floor aborts the command with exit 4.
- Per-project package IDs are
Legacy.{AssemblyName}, except that an assembly name already beginning withLegacy.is used unchanged —Legacy.Corepacks asLegacy.Core, notLegacy.Legacy.Core. The version is always1.0.0-wrapped. --max-projectsswitches to a single aggregate package namedLegacy.{SolutionName}(same prefix rule) when the project count exceeds the limit.--max-projects 0disables the limit (always per-project). Negative values are rejected..nuspecfiles and IL-rewritten assembly copies are written to a temporary directory that is deleted afterwards. Your original legacy binaries are never modified — IL rewriting always operates on a copy.
⚠️ Warning —
package --rewrite-ilis not self-contained. IL rewriting retargets call sites into bridge classes that onlygenerateorwrapcan produce. If you use it, the matchinggeneraterun must pass--remediate, or the wrapper compiles and then throwsTypeLoadException/MissingMethodExceptionat the first rewritten call.packagedoes not warn you about this. Preferwrap --rewrite-il, which always generates the bridges.
⚠️ Warning — missing binaries are skipped, not failed. Projects with no compiled assembly on disk are skipped. The console prints
Skipping {project} — no compiled assembly found.and aSkipped: N project(s) with no compiled assemblysummary line, and the command still exits 0 — even if every project was skipped and zero packages were produced. Always check thePackages: N createdline.
Exit codes: 0 success; 1 solution not found, or any other failure (including a negative --max-projects); 2 no build tool; 3 the legacy build failed; 4 a project targets below .NET Framework 4.6.1.
legacy-wrapper generate
Scaffold the .NET 10 wrapper solution against packages that already exist. Runs analysis internally but performs no build and no pre-flight check.
| Option | Alias | Type | Default | Required |
|---|---|---|---|---|
--solution |
-s |
file | — | Yes |
--packages |
-p |
directory | — | Yes |
--output |
-o |
directory | ./GeneratedWrapper |
No |
--remediate |
— | flag | False |
No |
--di-overrides |
— | flag | False |
No |
--ai-provider |
— | string | none | No |
--ai-api-key |
— | string | environment fallback | No |
--max-projects |
-m |
integer | 10 |
No |
--di-overridesimplies--remediate.--max-projectshere only decides whether the generated.csprojreferences one aggregate package or one package per project. It must match the value used atpackagetime. Unlikepackageandwrap, a negative value is silently treated as per-project mode rather than rejected.
⚠️ Important —
generatehas no blocker gate. Unlikewrap, it will generate a wrapper for a solution with unremediable blocking issues without complaint. Runanalyzefirst and read the report.
Exit codes: 0 success; 1 solution not found, packages directory not found, or any failure. generate never returns 2, 3, or 4.
legacy-wrapper wrap
The full pipeline: analyze, remediate, blocker gate, package, generate, write the HTML report.
| Option | Alias | Type | Default | Required |
|---|---|---|---|---|
--solution |
-s |
file | — | Yes |
--output |
-o |
directory | ./GeneratedWrapper |
No |
--skip-build |
— | flag | False |
No |
--configuration |
-c |
string | Release |
No |
--remediate |
— | flag | False |
No |
--rewrite-il |
— | flag | False |
No |
--di-overrides |
— | flag | False |
No |
--ai-provider |
— | string | none | No |
--ai-api-key |
— | string | environment fallback | No |
--max-projects |
-m |
integer | 10 |
No |
wrap packages into {--output}/local-packages itself, so it has no --packages option. It writes only an HTML report — use analyze --format all if you need JSON or Markdown.
ℹ️ Note —
--remediateis a no-op onwrap.wrapalways runs the remediation analysis (it needs it to classify blockers for the gate) and always passes the resulting plan into generation.wrapandwrap --remediateproduce identical output.--rewrite-iland--di-overridesdo change behavior.
Pipeline order is Analyze → Remediate → blocker gate → Package → Generate. The gate fires when at least one Blocking diagnostic matched no remediation strategy. In that case the offending items are listed on standard error, COMPATIBILITY-REPORT.html is written to the output directory, no wrapper is generated, and the command exits 4.
Exit codes: 0 success; 1 solution not found or any other failure; 2 no build tool; 3 the legacy build failed; 4 unremediable blocking issues or a project below .NET Framework 4.6.1.
legacy-wrapper doctor
Check build-tool prerequisites and show platform-specific install guidance. No command-specific options.
Exit codes: 0 when the .NET SDK or MSBuild is available; 1 when neither is found, or when probing throws.
Exit code contract
| Code | Meaning | Emitted by |
|---|---|---|
0 |
Success, or --help, or doctor with a build tool present |
all |
1 |
Input file or directory not found, or an unhandled failure. Also the parse-error code for a missing required option, and the code doctor uses when no build tool is found |
all |
2 |
No build tool found — pre-flight or mid-run | package, wrap |
3 |
The legacy solution build failed | package, wrap |
4 |
The wrapper cannot be produced: unremediable blocking issues (wrap only), or a project below .NET Framework 4.6.1 (package and wrap) |
package, wrap |
Code 4 has two distinct causes. Both mean the same thing operationally: nothing was generated, and the legacy side needs attention first.
Understanding the compatibility report
Score
score = 100 − (blocking × 25) − (critical × 10) − (warning × 2)
Clamped to the range 0–100. Info diagnostics contribute nothing.
| Score | Category |
|---|---|
| 90–100 | Highly Compatible |
| 70–89 | Mostly Compatible |
| 40–69 | Significant Work Required |
| 0–39 | Major Rewrite Required |
The score is calculated twice: once per project over that project's diagnostics, and once over the flat union of every project's diagnostics for the overall score. The overall score is therefore not an average of the project scores — a large solution accumulates penalties and floors at 0 quickly. Four blocking issues anywhere in the solution are enough to reach 0/100.
The analysis-incomplete signal
If Roslyn cannot load or compile a project, the tool records a SRC-* diagnostic and marks the whole report incomplete. The console shows:
Analysis incomplete for 2 of 7 project(s) — score is unreliable
See SRC-* diagnostics below for details.
The Markdown report carries the same warning, and the JSON report exposes analysisIncomplete and projectsWithFailedAnalysisCount as top-level fields — usable as a CI gate.
⚠️ Warning — an incomplete analysis barely dents the score.
SRC-*diagnostics are Warning severity, so a failed project costs 2 points (or 4, when a load failure and a missing compilation are both recorded for the same project). A solution where source analysis failed everywhere can still score in the 90s. If the incomplete banner is present, do not quote the score. Read theSRC-*entries first and fix the load problem. Binary analysis via Mono.Cecil runs independently and is the fallback signal, but only when compiled binaries exist on disk.
Severity levels
The console report tags each issue with the same markers used below.
| Tag | Severity | Meaning |
|---|---|---|
[BLK] |
Blocking | Cannot work at runtime. Must be addressed before the wrapper will function. Trips the wrap blocker gate when no bridge exists. |
[CRT] |
Critical | Will throw at runtime when the code path executes. |
[WRN] |
Warning | May work, but needs testing or has behavioral differences. |
[INF] |
Info | Informational. Does not affect the score. |
Diagnostic reference — Blocking
| ID | What triggered it | What to do |
|---|---|---|
TFM-001 |
A project targets .NET Framework below 4.6.1 | Retarget to 4.7.2 or 4.8, rebuild, rerun. Project-file change only. |
WEB-FORMS-001 |
.aspx/.ascx/.master files, or classes deriving from Page, UserControl, or MasterPage |
No .NET Core equivalent. Rewrite as Razor Pages or Blazor. Non-UI logic in the same solution can still be wrapped. |
ASMX-001 |
.asmx files, or [WebService] / [WebMethod] / WebService base class |
Convert each operation to an ASP.NET Core controller action (REST consumers) or a CoreWCF service with BasicHttpBinding (SOAP consumers). |
SW-001 … SW-008 |
System.Web API surface: HttpContext.Current, HttpApplication, HttpRequest, HttpResponse, HttpServerUtility, HttpSessionState, HttpCachePolicy, FormsAuthentication |
Migrate to the ASP.NET Core equivalent: IHttpContextAccessor, middleware or IHostedService, ASP.NET Core HttpRequest/HttpResponse, IWebHostEnvironment for path mapping plus WebUtility/HtmlEncoder for encoding, ISession, response caching or IDistributedCache, and cookie authentication middleware respectively. |
SW-GEN |
A System.Web member with no entry in the knowledge base |
Unmapped System.Web dependency. Review and migrate manually. |
CFG-002 |
Member access on a type in System.Web.Configuration |
Not available in ASP.NET Core. Use IConfiguration. |
RT-003 |
System.Runtime.Remoting usage |
Removed from .NET Core. Use gRPC, an ASP.NET Core Web API, or named pipes. |
PS-004 |
System.EnterpriseServices.ServicedComponent (COM+) |
Replace with standard dependency injection and System.Transactions transaction management. |
WCF-B01 |
netMsmqBinding in system.serviceModel config |
No CoreWCF equivalent. Replace with a message broker (Azure Service Bus, RabbitMQ) or a background job queue. |
WCF-B02 |
wsDualHttpBinding in config |
Duplex over HTTP is unsupported. Use SignalR or gRPC bidirectional streaming. |
WCF-B03 |
Binding security mode containing "Federation" or "Federated" | WS-Federation is not in CoreWCF. Migrate to OpenID Connect or OAuth 2.0. |
PKG-INCOMPAT |
A referenced NuGet package whose latest version on nuget.org only declares .NET Framework dependency groups | Replace with the recommended package when the report names one; otherwise find an alternative or reimplement. Requires network access — without it the package is scored Unknown rather than Incompatible. |
BIN-REF-001 … BIN-REF-008 |
An assembly-level reference found in the compiled binary: System.Web, System.Web.Services, System.Web.Mvc, Microsoft.Web.Infrastructure, System.Messaging, System.Runtime.Remoting, System.EnterpriseServices, System.Workflow.* |
These are the safety net for code whose source was never analyzed — vendored DLLs, IL-weaved assemblies, VB.NET and F# projects, projects whose Roslyn load failed. Each carries its own recommendation. Note that a BIN-REF entry is suppressed when source analysis already produced a diagnostic in the same category for that project, so the absence of BIN-REF-001 does not mean System.Web is absent — check the SW-* findings. |
Diagnostic reference — Critical
| ID | What triggered it | What to do |
|---|---|---|
SER-001 |
BinaryFormatter construction, Serialize, or Deserialize |
Bridge available. --remediate generates SerializationBridge over System.Text.Json with no extra package; --rewrite-il redirects the call sites. Review carefully — JSON differs from BinaryFormatter for private fields, cyclic references, and polymorphic types. |
SER-002 |
SoapFormatter |
Same bridge as SER-001. |
SER-003 |
NetDataContractSerializer |
Same bridge, or move to DataContractSerializer. |
SER-GEN |
A serializer type with no knowledge-base entry | Same treatment; also covered by the bridge. |
RT-001 |
AppDomain.CreateDomain |
--remediate generates AssemblyIsolationBridge over AssemblyLoadContext, but it is classified bridge-only — deliberately not IL-rewritten, so you wire it up by hand. Separate config files and security boundaries are not replicated. |
RT-002 |
Thread.Abort |
No bridge exists, deliberately. There is no safe automatic substitute. Rewrite to cooperative CancellationToken cancellation before wrapping. |
HTTP-MOD-001 |
One per module: classes implementing IHttpModule, and <httpModules> / <modules> entries in web.config |
ASP.NET Core has no IHttpModule pipeline, so the module's per-request logic (authentication, logging, URL rewriting) is silently dropped. Port each to middleware and register it in the wrapper's pipeline. |
GLOBAL-ASAX-001 |
An HttpApplication subclass with request-lifecycle handlers such as Application_BeginRequest, Application_Error, or Session_Start |
These are never invoked by ASP.NET Core, so the loss is silent. Port each to middleware. |
Diagnostic reference — Warning
| ID | What triggered it | What to do |
|---|---|---|
SRC-ERR |
Roslyn source analysis threw for a project | Treat that project's results as unreliable. See the analysis-incomplete section. |
SRC-NOCOMPILE |
Roslyn returned no compilation for a project | Same. The source analyzers never ran for that project. |
SRC-LOAD |
The workspace reported load failures, or loaded a project with zero documents | Most common on old non-SDK .csproj files and on macOS/Linux. Restore the solution first, and prefer running analysis on Windows with matching build tooling. |
CFG-001 |
ConfigurationManager or WebConfigurationManager member access |
Already handled — the generated LegacyConfigBridge bridges ConfigurationManager.AppSettings from appsettings.json at startup. Migrating to IConfiguration is the real fix. |
PS-001 |
Microsoft.Win32.Registry |
Bridge available: RegistryBridge over IConfiguration, and IL-rewritable. Without it, registry calls throw PlatformNotSupportedException on Linux and macOS containers. |
PS-002 |
System.Management (WMI) |
Windows-only. Use a cross-platform alternative or platform-conditional code. |
PS-003 |
[DllImport] in source, or P/Invoke methods found in the compiled IL |
Ensure the native library ships in the container image, or replace with a cross-platform API. |
PS-005 |
System.Drawing types |
Bridge available: ImageProcessingBridge over SixLabors.ImageSharp, IL-rewritable. Carries a license obligation — see the remediation section. Covers load, save, and resize; advanced GDI+ work (gradients, complex paths) needs manual review. |
PS-GEN |
A platform-specific namespace member with no knowledge-base entry | Review platform compatibility for your target environment. |
PS-BIN |
The compiled binary references System.Management or System.ServiceProcess |
Windows-only dependency. Provide a cross-platform alternative, or switch the generated Dockerfile to Windows container images (see What gets generated). |
PS-PINVOKE |
Aggregate count of P/Invoke methods in an assembly, one entry per assembly | Companion to PS-003. Use it to size the native-dependency work. |
RT-004 |
MarshalByRefObject |
The type exists on .NET 10, but remoting and AppDomain scenarios do not work. Review usage. |
SW-009, SW-010 |
System.Web.Mvc.Controller, System.Web.Http.ApiController |
The wrapper generates proxy controllers. Review action return types, content negotiation, and model binding. |
BIN-REF-009 |
The compiled binary references System.Transactions |
System.Transactions.Local exists, but escalation to distributed or MSDTC transactions throws PlatformNotSupportedException off Windows. Verify no path escalates; restructure to single-connection transactions or an outbox pattern. |
WCF-W01 |
webHttpBinding in config |
Not a blocker. REST-style WCF should become ASP.NET Core controllers, and the tool generates them from [WebGet] and [WebInvoke] attributes. |
GLOBAL-ASAX-002 |
An HttpApplication subclass whose only convention methods are startup or shutdown (Application_Start, Application_End, Application_Init, Application_Disposed) |
Move that initialization into the wrapper's Program.cs before the host starts, or into an IHostedService. |
Diagnostic reference — Info
| ID | What triggered it | Note |
|---|---|---|
ROUTE-001 |
Programmatic RouteTable.Routes registrations in a Global.asax class |
Detected but not translated into wrapper endpoints. Recreate them with MapControllerRoute in Program.cs. Info severity means it does not reduce the score — do not skim past it. |
WCF-I01, WCF-I02, … |
One per distinct binding type that migrates directly to CoreWCF: BasicHttpBinding, NetTcpBinding, WSHttpBinding, NetNamedPipeBinding | Confirmation, not a problem. The number after WCF-I is positional and depends on how many supported bindings your solution has — it is not a stable identifier. |
BIN-ERR |
A compiled assembly could not be read by the binary analyzer | Binary analysis was skipped for that file. Worth investigating despite the Info severity — a native or mixed-mode DLL will not load on .NET 10 either. |
What gets generated
Wired up versus scaffolding
Read this before you evaluate the output.
| Generated artifact | State |
|---|---|
WrapperHost.csproj |
Working. Targets net10.0 and references your legacy packages at version 1.0.0-wrapped, plus compatible modern third-party packages and CoreWCF where needed. |
nuget.config pointing at the local feed plus nuget.org |
Working. |
LegacyConfigBridge.cs and appsettings.json |
Working, with placeholders. Non-secret <appSettings> values are migrated and bridged back to ConfigurationManager.AppSettings at startup. Connection strings and likely-secret keys are placeholdered — see below. |
CoreWCF hosting (CoreWcfExtensions.cs) |
Working. Registers and exposes your actual legacy service types and contracts. This is the one path where your legacy code genuinely executes without a manual step. |
DI wiring (ServiceCollectionExtensions.cs) |
Working, derived from your detected IoC container registrations. Review it. |
Remediation bridges (Remediation/*.cs) |
Working, but review them — a bridge is a behavioral substitution, not an identity. |
Program.cs, Dockerfile, docker-compose.yml |
Working. Multi-stage build on mcr.microsoft.com/dotnet/sdk:10.0 and mcr.microsoft.com/dotnet/aspnet:10.0 — Linux images, exposing port 8080. If your report contains PS-002 (WMI), PS-003 (P/Invoke to Windows DLLs), PS-BIN, or unremediated PS-001/PS-005, switch both FROM lines to the -windowsservercore-ltsc2022 tags and run on a Windows container host. |
Web API / MVC proxy controllers (Controllers/*.cs) |
Scaffolding. Route templates and HTTP-method attributes are correct. Parameter signatures are stubbed — route tokens are typed string, request bodies are a single [FromBody] object?, and query-string parameters are omitted entirely. Each action body throws NotImplementedException. |
HTTP handler proxies (Handlers/*.cs) |
Scaffolding. Same pattern, plus generated warning comments where the handler used session or authentication. |
REST-style WCF controllers (from webHttpBinding) |
Scaffolding. Same pattern, and no body parameter is generated at all — add one for any operation that took a request payload. |
DI override adapter classes (--di-overrides) |
Stubs unless an AI provider is configured — see the remediation section. |
⚠️ Important — endpoint scaffolding, not endpoint migration. Expect roughly one wire-up edit per HTTP endpoint, including retyping the generated parameters to match the legacy signature. This is manual for a structural reason: legacy
ApiControllerandIHttpHandlertypes derive fromSystem.Webtypes that cannot be loaded on .NET 10, so the generated proxy cannot instantiate them. You point each action at the underlying business-logic classes instead, resolving them through the generated DI wiring.One subtlety worth knowing: in the Web API and MVC proxy controllers, actions whose legacy counterpart returned
voidare generated asreturn Ok();rather than a throw. Those endpoints silently do nothing instead of failing loudly. When planning the work, search the generatedControllers/andHandlers/folders forNotImplementedExceptionand forreturn Ok();— the second group is easy to miss because it never errors.
Configuration and secrets
Every connection string value is replaced with REPLACE_WITH_ACTUAL_CONNECTION_STRING_FOR_{NAME}. appSettings keys matching any of thirteen secret patterns — including password, pwd, secret, apikey, token, credential, clientsecret, accesskey, and privatekey — are replaced with REPLACE_WITH_ACTUAL_VALUE_FOR_{KEY}.
This is deliberate: the tool refuses to copy credentials into a generated file. But it means the wrapper will not connect to anything until you supply real values through environment variables, user secrets, or your deployment platform's secret store.
⚠️ Warning — the scrubbed keys are not listed anywhere. The tool computes them internally but does not print them. After generating, open
<your --output directory>/src/WrapperHost/appsettings.jsonand search forREPLACE_WITH_ACTUALby hand. Nothing else will tell you.
A second gotcha: ConfigurationManager.ConnectionStrings is read-only at runtime, so it cannot be bridged the way AppSettings is. Legacy code that reads connection strings needs updating to use IConfiguration.GetConnectionString(). The generated bridge documents this in a comment at the relevant spot.
Layout
GeneratedWrapper/
├── WrapperHost.sln
├── nuget.config # LocalLegacyPackages → ./local-packages, plus nuget.org
├── Dockerfile
├── docker-compose.yml
├── COMPATIBILITY-REPORT.html
├── local-packages/
│ └── Legacy.*.1.0.0-wrapped.nupkg # copied from the packaging feed
└── src/
└── WrapperHost/
├── WrapperHost.csproj # net10.0
├── Program.cs
├── appsettings.json
├── Configuration/
│ └── LegacyConfigBridge.cs
├── Controllers/ # one file per Web API endpoint and per REST-style WCF service
├── Handlers/ # one file per discovered IHttpHandler
├── Infrastructure/
│ ├── LegacyHttpContextAdapter.cs # only when handlers were discovered
│ ├── CoreWcfExtensions.cs # only when WCF services were discovered
│ └── ServiceCollectionExtensions.cs # only when IoC containers were detected
└── Remediation/ # only when bridge classes or DI adapters were generated
The wrapper project name is fixed at WrapperHost. The four subdirectories Controllers/, Handlers/, Infrastructure/, and Configuration/ are always created, even when empty. Remediation/ is not — it appears only when there was actually something to bridge: on any wrap run (which always remediates), or on a generate run with --remediate or --di-overrides.
Remediation and IL rewriting
--remediate
Available on generate and wrap. Generates bridge classes under <your --output directory>/src/WrapperHost/Remediation/ that provide modern implementations of APIs your legacy code depends on:
| Diagnostic | Bridge class | Replacement | Extra package |
|---|---|---|---|
SER-001, SER-002, SER-003 |
SerializationBridge |
System.Text.Json |
None (in-box) |
PS-005 |
ImageProcessingBridge |
SixLabors.ImageSharp 3.1.12 | Yes — see the license note below |
PS-001 |
RegistryBridge |
IConfiguration, reading the LegacyRegistry section of appsettings.json |
None |
RT-001 |
AssemblyIsolationBridge |
AssemblyLoadContext |
None |
CFG-001 |
LegacyConfigBridge |
Already generated as part of the standard config bridge; no additional class | None |
Generating a bridge does not change your legacy code. It makes a modern implementation available; either you call it, or --rewrite-il points the existing call sites at it.
Remember that on wrap the remediation analysis always runs, so --remediate there is a no-op. On generate the flag is real.
⚠️ Warning — SixLabors.ImageSharp license obligation. When
PS-005is remediated, the generated wrapper takes a dependency on SixLabors.ImageSharp 3.1.12, which is distributed under the Six Labors Split License — free for open-source projects and small organizations, but commercial use above the revenue threshold requires a paid license. See https://sixlabors.com/pricing/ for current terms. The tool surfaces this note on the console and in the report; treat it as a purchasing consideration, not a footnote.If you cannot accept those terms, omit
--remediate(thePS-005bridge is only generated when you ask for remediation), or deletesrc/WrapperHost/Remediation/ImageProcessingBridge.csand theSixLabors.ImageSharp<PackageReference>from the generatedWrapperHost.csprojand handleSystem.Drawingusage yourself. Do not combine that removal with--rewrite-il, which retargets call sites into the bridge you just deleted.
--rewrite-il
Available on package and wrap. Uses Mono.Cecil to rewrite call sites in your compiled assemblies so that blocked API calls target the generated bridge methods instead.
Nine rules exist, covering exactly five diagnostics: PS-001 (registry reads), SER-001, SER-002, SER-003 (serializer Serialize and Deserialize), and PS-005 (Image.FromFile and Image.FromStream). Nothing else is IL-rewritable. RT-001 is deliberately excluded — replacing AppDomain.CreateDomain automatically is too dangerous.
Safety properties worth knowing:
- Your original binaries are never modified. The rewriter always works on a copy in a temporary directory, and the rewritten copy is what gets packaged.
- Rewriting fails soft. If an assembly cannot be rewritten, the tool prints a warning and continues with the original assembly. A successful overall run therefore does not guarantee that every assembly was rewritten — read the
Rewrote N IL call(s) in {project}lines. - On
wrap, when the remediation analysis found DI-level substitutions, the diagnostics those cover are excluded from IL rewriting so the two mechanisms do not both fire. The tool printsSkipping IL rewrite for N diagnostic(s) covered by DI substitution. wrap --rewrite-ilis safe, becausewrapalways generates the bridge classes the rewritten calls target.package --rewrite-ilis not self-contained — the matchinggeneraterun must pass--remediate, as described in the split-pipeline warning above.
--di-overrides
Available on generate and wrap, implies --remediate. When your legacy code registers an affected type in an IoC container, this generates partial adapter classes that substitute the bridge implementation at the DI level rather than at the call site. Without it, affected registrations get advisory comments only.
The generated adapters are TODO stubs that you fill in — unless you also configure an AI provider.
--ai-provider and --ai-api-key
Optional. Sends code context to an external LLM (Anthropic or OpenAI) to fill in the DI adapter bodies instead of leaving stubs.
--ai-provideracceptsanthropicorclaudefor Anthropic, andopenaiorgptfor OpenAI. Matching is case-insensitive; any other value is treated as unconfigured.--ai-api-keyoverrides theANTHROPIC_API_KEY/OPENAI_API_KEYenvironment variables. If no explicit key is given,ANTHROPIC_API_KEYis tried first (defaulting the provider toanthropic), thenOPENAI_API_KEY.
⚠️ Warning — three things to know before enabling this.
- It sends your code context to a third-party API. Check that against your organization's policy first.
- The output is not compile-validated. Review every generated adapter.
- Both flags are silently ignored without
--di-overrides, and an unrecognized provider name or an unresolvable API key silently downgrades to TODO stubs with no error message. The only positive confirmation is the console lineAI adapter generation enabled ({provider}).— if you do not see it, AI generation did not run.
Troubleshooting
Exit 2 — "No build tool found"
Cause. Neither the .NET SDK nor MSBuild was found. The pre-flight check runs before any work in package and wrap, so nothing was wasted.
Fix. Run legacy-wrapper doctor and follow the printed guidance. On Windows install Visual Studio 2022 Build Tools with the managed-desktop workload (see Requirements); on macOS or Linux install the .NET SDK, and for classic Framework projects install Mono's msbuild — or build the solution elsewhere and pass --skip-build, which bypasses the check entirely.
Exit 3 — "Build failed"
Cause. A build tool was found, but building your legacy solution failed. The full standard output and standard error from the build are included in the message.
Fix. Build the solution in your normal IDE or CI first, confirm it succeeds there, then rerun with --skip-build.
If you are on macOS or Linux with only the .NET SDK, this is the expected outcome for classic non-SDK .csproj files. dotnet build handles SDK-style projects and is limited for legacy ones.
Note that --configuration is passed to the build only — it does not steer where the tool looks for binaries afterwards. See "Packages: 0 created" but exit code 0 below.
Exit 4 — "Unsupported target framework"
Cause. At least one project targets .NET Framework below 4.6.1. Message: Cannot package: N project(s) target a .NET Framework version below the 4.6.1 minimum required for netstandard2.0 packaging: …
Fix. Retarget the named projects to 4.7.2 or 4.8, rebuild, rerun. This is a project-file change, not a code migration.
Exit 4 — unremediable blocking issues (wrap)
Cause. At least one Blocking diagnostic matched no remediation strategy. The tool lists each as [ID] Title followed by its recommendation, on standard error.
Fix. COMPATIBILITY-REPORT.html was written to your output directory even though no wrapper was generated — that report is the work list. Address the listed items in the legacy application (or accept them as scoped manual work) and rerun. Common culprits are WEB-FORMS-001, ASMX-001, TFM-001, SW-001 through SW-008, and WCF-B01 through WCF-B03.
Exit 1 — "Solution file not found" / "Packages directory not found"
Cause. The path passed to --solution or --packages does not exist.
Fix. Check the path. --packages must point at a directory that already contains .nupkg files, which normally means running package first.
"Packages: 0 created" but exit code 0
Cause. Every project was skipped because no compiled assembly was found. Relative to each project's own directory, the tool probes the explicit OutputPath from the .csproj if one is set, then bin/Release, bin/Debug, bin/Release/{tfm}, and bin/Debug/{tfm}. It looks for {AssemblyName}.dll for library projects and {AssemblyName}.exe for everything else.
Fix. Build the legacy solution first, or drop the binaries into one of those locations, then check the Skipped: N project(s) line. A silently empty feed is the failure mode here — the exit code will not tell you.
⚠️ Warning — the probe order is fixed and ignores
--configuration. That flag reaches the build invocation only. Debug-only binaries are therefore found under the default--configuration Release, which is convenient — but the reverse is the hazard: a stalebin/ReleaseDLL always wins over a freshly builtbin/Debugone, silently packaging old code. If you build Debug, either cleanbin/Releaseor point the project'sOutputPathat the output you actually want.
"Omitted dependency … from the nuspec"
Cause. A declared package dependency had no matching assembly in the legacy build output, so it was left out of the generated .nuspec.
Fix. Usually means the legacy solution was not fully restored or built. The message names the dependency and states that the wrapped package may fail at runtime if that dependency is required. Restore and rebuild, then repackage.
"Analysis incomplete for N of M project(s) — score is unreliable"
Cause. Roslyn could not load or compile some projects — SRC-LOAD, SRC-ERR, or SRC-NOCOMPILE. Most common with old non-SDK project files and when running on macOS or Linux. Also check that you have the .NET 10 SDK installed and not just the runtime — MSBuild is unavailable without it, and every project then fails to load.
Fix. Restore the solution (nuget restore or dotnet restore) so package references resolve, then rerun. If it persists, run analyze on Windows with matching build tooling. Binary analysis still runs independently when compiled binaries are present, so packaging decisions are not blind — but do not quote the compatibility score from an incomplete report.
A VB.NET or F# project reports no findings at all
Cause. Expected, and it is not a diagnostic. Source analysis is C# only. .vbproj and .fsproj projects load, get packaged, and get binary-scanned, but no source analyzer, endpoint discovery, WCF discovery, or IoC discovery runs on them, and nothing warns you.
Fix. Rely on the BIN-REF-*, PS-BIN, and PS-PINVOKE findings for those projects, and review their source by hand. Do not read a clean score on a VB/F# project as a compatibility signal.
"Unknown format" and no report file
Cause. --format received a value other than html, json, md, or all. The analysis ran to completion, nothing was written, and the exit code was 0.
Fix. Use one of the four valid values. Confirm success by looking for the HTML report: … line.
The wrapper builds but every endpoint throws NotImplementedException
Cause. Expected. Proxy controllers and handler proxies are scaffolding — see the What gets generated section.
Fix. Wire each action to the corresponding business-logic class, resolved through the generated DI wiring in Infrastructure/ServiceCollectionExtensions.cs, and retype the generated parameters to match the legacy signature. Do not try to instantiate the legacy ApiController or IHttpHandler type itself — it cannot load on .NET 10.
The wrapper starts but cannot reach the database
Cause. Connection strings were placeholdered, not copied.
Fix. Open ./GeneratedWrapper/src/WrapperHost/appsettings.json (or the equivalent path under your own --output directory), replace every REPLACE_WITH_ACTUAL_CONNECTION_STRING_FOR_* and REPLACE_WITH_ACTUAL_VALUE_FOR_* value with a real value (preferably from environment variables or a secret store), and update legacy code that reads ConfigurationManager.ConnectionStrings to use IConfiguration.GetConnectionString() instead.
TypeLoadException or MissingMethodException at runtime after IL rewriting
Cause. You ran package --rewrite-il and then generate without --remediate. The rewritten call sites target bridge classes that were never generated.
Fix. Regenerate with generate --remediate, or use wrap --rewrite-il, which always generates bridges.
Diagnosing anything else
legacy-wrapper wrap -s ./Legacy.sln -o ./GeneratedWrapper --log-file lw.log
The console stays terse, but lw.log receives everything plus the full exception with type, stack trace, and inner exceptions — even without --verbose. Add -v if you also want the stack trace on screen. The log file is overwritten on each run.
Please attach lw.log when reporting a problem.
Limitations
An honest summary of what this tool does not do. None of these are bugs; they are boundaries.
1. HTTP endpoint proxies are scaffolding. Route templates and HTTP-method attributes are generated correctly. Parameter signatures are not: route tokens come out as string, request bodies as [FromBody] object?, and query-string parameters are not generated at all. The call into your business logic is manual, one edit per endpoint, for the structural reason described above. Budget accordingly.
2. CoreWCF hosting is the only path that executes legacy code with no manual step. If your application's value sits in WCF services classified as directly migratable, you get the most out of this tool. If it sits in Web API and MVC controllers, you get analysis plus scaffolding.
3. Connection strings and detected secrets are never copied. The wrapper will not connect to anything until you supply real values, and the list of scrubbed keys is not printed — inspect appsettings.json manually.
4. .NET Framework 4.6.1 is a hard, enforced floor. Below it, packaging is refused with exit code 4.
5. Web Forms and ASMX are detected and blocked, not wrapped. For a 2010-era ASP.NET application this is the most likely reason wrap stops.
6. HTTP modules and Global.asax lifecycle handlers do not run in the wrapper. They are reported as Critical (HTTP-MOD-001, GLOBAL-ASAX-001) precisely because the loss would otherwise be silent. Port each to ASP.NET Core middleware.
7. Programmatic routes are detected but not translated. ROUTE-001 is Info severity; recreate those routes with MapControllerRoute in Program.cs.
8. Thread.Abort has no bridge, by design. There is no safe automatic substitute. Rewrite to cooperative cancellation first.
9. AI adapter generation is opt-in, sends code to a third party, and is not compile-validated. Failures degrade silently to TODO stubs.
10. Analysis is best-effort and says so. If Roslyn cannot load a project you get SRC-* warnings and an incomplete banner. Do not read a high score off an incomplete report.
11. Binary-level dependency checks require compiled binaries and, for third-party package compatibility, network access to nuget.org. Without network access, package compatibility degrades to Unknown rather than failing.
12. The generated Dockerfile targets Linux containers. If your report keeps Windows-only diagnostics (PS-002, PS-003, PS-BIN, unremediated PS-001/PS-005), you must switch it to Windows container images yourself.
13. Source analysis is C# only. VB.NET and F# projects are packaged and binary-scanned, but no source-level analyzer runs on them and no warning is emitted. Their reported score reflects binary analysis alone.
14. This is a preview release — see Status at the top of this page.
Support
Support is direct from the author, on a best-effort basis — there is no service-level commitment attached to this preview release.
For bug reports, feature requests, or help wrapping a specific solution, contact Michael Matskevich at mihadeth@gmail.com.
To make the first reply useful, rerun the failing command with --log-file lw.log and include: the tool version (legacy-wrapper --version), your OS, the exact command you ran, your legacy solution's target framework, and lw.log itself. That log holds the full exception detail — inner exceptions and stack traces — which the console deliberately omits.
The incompatibility knowledge base — the API mappings, third-party package equivalents, and WCF feature matrix that drive the analysis — ships inside the tool itself. If you have a mapping to contribute or a missing incompatibility to report, send it to the address above and it will be considered for the next release.
License
MIT. Copyright (c) 2026 Michael Matskevich.
Note that remediation bridges may introduce third-party dependencies with their own license terms. In particular, --remediate on a PS-005 diagnostic adds SixLabors.ImageSharp under the Six Labors Split License — see the remediation section above.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.3 | 83 | 8/2/2026 |
| 1.0.0-preview.2 | 75 | 8/2/2026 |