MigrationScan.Tool 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet tool install --global MigrationScan.Tool --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local MigrationScan.Tool --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=MigrationScan.Tool&version=0.1.0
                    
nuke :add-package MigrationScan.Tool --version 0.1.0
                    

MigrationScan

A free, deterministic, offline .NET Framework migration assessment tool.

It answers one question: how much work is it to move this solution off .NET Framework, and what specifically blocks it? It runs offline, produces the same output every time, requires no account, and never transmits your source code.

Download the executable for your platform from the releases page, put it in the folder you want to assess, and run it:

migrationscan path/to/YourSolution.sln

It writes one file — migrationscan-report.json — and prints a summary. No .NET install, no admin rights, no flags to learn. With no arguments at all it scans the current directory, so dropping the executable into a repository root and double-clicking it works too.

Already have the .NET 10 SDK? dotnet tool install -g MigrationScan.Tool gets you the same tool.

Status: pre-release / under active development. The foundation is in place (Phase 0). Rules, reports, and the CLI surface are being built out phase by phase — see Roadmap. The install command above will work once the first release is published to NuGet.

📄 See a sample Markdown report → — the shareable artifact an engineering manager forwards to a CTO: executive summary, blockers, findings by project, an effort breakdown, and remediation guidance.

Why this exists

Microsoft deprecated the .NET Upgrade Assistant in late 2025 and replaced it with the GitHub Copilot app modernization agent. The replacement needs a paid subscription and sends source code to a cloud service. Teams in finance, healthcare, defense, and government cannot do that — and they are the teams sitting on the largest .NET Framework estates. MigrationScan gives those teams an assessment they can run themselves, offline, with nothing leaving the machine.

The promise

  • Offline by default. No network calls in the default path. Network access is only available behind an explicit --online flag, and only for NuGet package compatibility lookups.
  • No telemetry. No phoning home, no usage collection, no login.
  • Your code stays put. Source is never transmitted anywhere.
  • Deterministic. Same input, same output, every run.
  • No AI in the analysis path. Findings come from static analysis, not an LLM.

Non-goals

MigrationScan deliberately does not:

  • Modify your source code
  • Perform the upgrade
  • Use AI or an LLM anywhere in the analysis path
  • Phone home, collect telemetry, or require a login
  • Produce a binding cost estimate — effort figures are heuristic planning aids, not a quote
  • Require Visual Studio or MSBuild to be installed
  • Replace human judgment on architectural decisions

How it works

MigrationScan parses your .sln, .csproj, and .vbproj files as XML and reads your .cs and .vb source with Roslyn — no MSBuild registration and no Visual Studio required, so it runs the same on Windows, Linux, and macOS. Every finding carries a confidence tier so the report is honest about what static analysis can and cannot prove:

Tier Name Source
1 Certain Project files, packages.config, app.config, web.config, .sln (XML, no ambiguity)
2 Probable Roslyn syntax trees without a resolved compilation (good recall, some false positives)
3 Verified Read from compiled assemblies via Cecil — see binary analysis

Reference inventory

Findings tell you what's broken. The reference inventory tells you what you depend on — every NuGet package, GAC and vendored assembly, COM/ActiveX component, project reference, and ASMX/WCF service proxy each project declares, with versions and where each one resolves from.

It's inventory, not findings: no severity, no effort, never a build failure. The point is that the expensive unknowns in a migration are usually somebody else's code — a control from a vendor that folded, a type library with no 64-bit build — and rules can only flag the ones with a known pattern. The inventory gives you the rest of the list to research.

Third-party references (10 distinct) — inventory, not counted above:
  • nuget   Newtonsoft.Json 13.0.3
  • gac     Telerik.Web.UI 2015.3.930.45
  • dll     Contoso.Payments 2.1.0.0
  • com     AxInterop.MSCommLib 1.0.0.0
  • com     MSXML2 6.0
  • svc     PricingService
  (Also read, not listed: 1 framework, 1 solution-internal.)

The Markdown report renders it as a table with per-component project counts; the JSON exposes it as a references array for scripting. See the references doc for what each kind covers, the classification judgment calls, and what deliberately isn't collected.

Rules

MigrationScan ships a catalog of stable, never-reused rule IDs grouped by category (project/build, dependencies, blocking frameworks, runtime failures, configuration, serialization/security, data access, globalization). Each rule links to a remediation page under /docs/rules.

Implemented rules

ID Rule Severity Tier
MIG1001 Non-SDK-style project file Medium 1 — Certain
MIG1002 packages.config instead of PackageReference Medium 1 — Certain
MIG1003 Target framework below 4.6.2 Medium 1 — Certain
MIG1005 GAC reference (no HintPath) Medium 1 — Certain
MIG1006 COM reference or interop assembly (Windows lock-in) Medium 1 — Certain
MIG1007 Legacy project type (SSRS, SSIS, setup, Silverlight, Web Site) High 1 — Certain
MIG1010 Vendored DLL with no source and no NuGet equivalent High 1 — Certain
MIG2001 Package has no version supporting the target framework High 1 — Certain
MIG2002 Package marked deprecated on nuget.org (--online) Medium 1 — Certain
MIG3001 ASP.NET WebForms Blocker 1 — Certain
MIG3002 System.Web dependency outside WebForms High 1 — Certain
MIG3003 ASMX web service High 1 — Certain
MIG3004 WCF service host (server side) High 2 — Probable
MIG3005 .NET Remoting Blocker 2 — Probable
MIG3009 MSMQ (System.Messaging) High 2 — Probable
MIG3010 ASP.NET MVC 5 (System.Web.Mvc) High 2 — Probable
MIG3015 WCF client (System.ServiceModel) Medium 2 — Probable
MIG4001 System.Drawing.Common on non-Windows High 2 — Probable
MIG4002 Windows Registry access High 2 — Probable
MIG4003 System.Management / WMI High 2 — Probable
MIG4004 System.DirectoryServices / Active Directory High 2 — Probable
MIG4005 EventLog Medium 2 — Probable
MIG4008 Thread.Abort Medium 2 — Probable
MIG4013 P/Invoke to a Windows system DLL (Windows lock-in) Medium 2 — Probable
MIG5001 ConfigurationManager.AppSettings usage Low 2 — Probable
MIG6001 BinaryFormatter (removed in .NET 9) Blocker 2 — Probable
MIG6004 Code Access Security attributes Medium 2 — Probable
MIG6005 Obsolete cryptography types Medium 2 — Probable
MIG7001 System.Data.SqlClient Medium 2 — Probable
MIG7003 System.Data.OleDb on non-Windows Medium 2 — Probable
MIG7006 LINQ to SQL (System.Data.Linq) High 2 — Probable
MIG8002 Encoding.Default behavior change Medium 2 — Probable
MIG8003 Code-page encoding without provider registration Medium 2 — Probable

More rules land phase by phase; see the full catalog in the spec.

Usage

migrationscan [path] [options]

  [path]                  .sln, .csproj, .vbproj, .dll/.exe, or directory to scan.
                          Defaults to the current directory.

  --target <tfm>          .NET version to assess against (default: net10.0). Both
                          portability stances are always reported — see below.
  --format <fmt>          console | markdown | json | sarif (repeatable). Omit for the
                          default: a console summary plus migrationscan-report.json.
  --output <path>         Output file or directory
  --fail-on <severity>    blocker | high | medium | low
  --online                Allow NuGet.org lookups for package compatibility
  --baseline <path>       Suppress findings present in a baseline file
  --include-paths         Keep real file paths in the JSON (off by default)

That is the whole surface — --help is the authority. --fail-on is evaluated against the stance --target names, so adding the second stance to the report never changes an exit code.

console always writes to stdout. For json/markdown, --output may be a file (written as-is for a single format) or a directory (receives report.json / report.md). When several file formats share one --output file path, each is written with its own extension so they don't overwrite each other.

Cross-platform vs. staying on Windows

Modern .NET can still target Windows (net10.0-windows), where COM interop, P/Invoke to Win32, the Registry, WMI, and similar continue to work. Those APIs are Windows lock-in — they are only migration cost if you also need to leave Windows.

You do not have to choose, and you do not have to scan twice. Every report covers both stances:

"targets": [
  { "target": "net10.0",         "stance": "crossPlatform", "summary": { /* 7.8–23 days */ } },
  { "target": "net10.0-windows", "stance": "windows",       "summary": { /* 5–15 days  */ } }
]

The difference between those two numbers is the price of portability, and it comes from a single scan: the target changes what a finding costs, never what was found, so the second stance is an exact re-evaluation rather than a second analysis.

The console and Markdown reports show the stance named by --target (cross-platform by default, the loud one), where Windows lock-in findings are downgraded: still listed under a "satisfied by target" section and flagged satisfiedByTarget in JSON / suppressed in SARIF, but excluded from the severity counts, the effort estimate, and --fail-on. Gone-everywhere findings — WebForms, BinaryFormatter, Remoting, MVC 5, and the rest — stay at full severity regardless of stance.

--target selects the .NET version (net8.0 vs net10.0); the platform axis is always reported both ways. If you want the console summary from the Windows stance instead:

migrationscan MyApp.sln --target net10.0-windows

The JSON is identical either way — both stances are in it regardless.

Exit codes

Code Meaning
0 No findings above threshold
1 Findings above --fail-on threshold
2 Analysis error
64 Bad usage

Online package checks (--online)

By default MigrationScan makes no network calls and the output is fully deterministic. Passing --online opts in to nuget.org lookups for package status — currently flagging packages the maintainers have marked deprecated (MIG2002):

migrationscan . --online

Because these findings reflect live nuget.org state, they are not part of the deterministic default path. If a lookup fails (offline, rate-limited), the scan degrades gracefully — it prints a warning and continues without package status rather than failing.

Scanning compiled binaries

When you don't have the source — a third-party component, or an early look at a client's build output — point MigrationScan at a compiled assembly:

migrationscan path/to/YourApp.dll

It reads the assembly with Mono.Cecil and flags references to assemblies that aren't available on modern .NET (System.Web, System.Drawing, System.Management, System.Messaging, …). These are Tier 3 — Verified findings: read from the compiled metadata rather than inferred from syntax. Source-based scanning (a .sln/.csproj) remains richer; binary analysis is the fallback for when source isn't on hand.

Continuous integration

MigrationScan is built for CI: machine-readable output, meaningful exit codes, and no interactive prompts.

GitHub code scanning

Emit SARIF and upload it — findings show up inline on the Security → Code scanning tab and as annotations on pull requests:

name: Migration scan
on: [push, pull_request]

permissions:
  contents: read
  security-events: write   # required to upload SARIF

jobs:
  migrationscan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - run: dotnet tool install -g MigrationScan.Tool
      - run: migrationscan . --format sarif --output migrationscan.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: migrationscan.sarif

SARIF file paths are relative to the scan root (the directory or solution you point at), so run the scan from the repository root for the annotations to line up with your files.

Failing the build on regressions

Use --fail-on to return exit code 1 when a finding is at least as severe as the threshold:

migrationscan . --fail-on high        # fail on any high or blocker finding

Baselining an existing estate

Adopt the tool on a large legacy codebase without failing on day one: capture a baseline, then fail only on new findings.

migrationscan . --format json --output migrationscan-baseline.json   # once, committed to the repo
migrationscan . --baseline migrationscan-baseline.json --fail-on high # in CI: only new findings count

A baseline is just a JSON report captured earlier; findings whose rule, file, and message match one in the baseline are suppressed. (Line numbers are ignored, so baselined findings survive unrelated edits that shift lines.)

Building on the output

The JSON is a stable, versioned, deterministic feed meant to be consumed by other tools — dashboards, portfolio rollups, or your own scoping/estimating layer. Alongside the findings it carries an effort rollup (summary.effort and a per-projects breakdown, in engineer-day ranges) and a notAssessed list of non-C#/VB projects (SQL, deployment, …) that need planning of their own — so coverage gaps are explicit, not silent. It also carries the full references inventory, flat and per-project, for feeding a dependency-research workflow. See the output schema for the full shape and consumer notes. Effort figures are heuristic planning aids, not a quote — apply your own rates and judgment downstream.

What's in the report (for your security review)

The JSON report contains no file paths. Each one is replaced by a stable opaque id, so the file can be shared without anyone reading several thousand lines of JSON to approve it. Every run says so, and every Markdown report ends with a "What this report contains" section.

It includes: project names, line numbers, rule identifiers and their fixed remediation text, and the names and versions of dependencies your projects declare (NuGet packages, referenced assemblies, COM components, web-service endpoints, Windows system libraries called via P/Invoke). Those are identities, not locations, and they are kept deliberately — a component cannot be assessed without knowing which one it is.

It does not include: source file paths, any source code or file contents, connection strings, credentials, configuration values, web-service hosts and URLs, customer or business data, machine or user names, or anything from outside the folder you scanned.

Redaction applies to the JSON — the file you share. The console, the Markdown report and the SARIF output keep full paths on purpose: they stay on your machine, SARIF exists to annotate a specific line in a specific file, and withholding paths from your own developers would help nobody. --include-paths keeps them in the JSON too.

Two details worth knowing: a fileId is stable, so two findings in the same file still visibly share a file — real signal for estimating, at no disclosure. And a redacted report still works as a --baseline, because each finding records its own fingerprint rather than having one derived from the path.

Each report also records what produced it — the tool version, and the commit the scanned tree was checked out at when it is a git working tree:

"scan": { "toolVersion": "0.1.0", "commit": "0fc6524d7b26ccd2f1eca0d18d8b3792dc6dc675" }

There is deliberately no timestamp: the report is byte-identical for the same input by design, which is what makes it diffable and baselineable. The commit is the better answer to "is this scan stale?" anyway, since it names the exact revision assessed.

Scanning a whole estate

Point MigrationScan at a directory and it assesses everything under it in one pass — every solution, and every project, in one report:

migrationscan C:\code\LegacyEstate

Projects are the unit of truth and solutions are the grouping, not the other way round. A project is assessed because it exists on disk, so a project no solution references is still scanned — those are exactly the ones that surface halfway through a migration and blow the plan. They are called out in the warnings so you can confirm whether they are in scope. A project shared by several solutions is scanned once, not once per solution.

Build output, restored packages/, node_modules, and dot-directories are skipped, so a vendored source tree is never mistaken for your own code.

Limitations

Static analysis without resolved references cannot see everything, and MigrationScan is honest about that rather than pretending to certainty:

  • Tier 2 findings can be false positives. A reference to a type named Registry might be your own class, not Microsoft.Win32.Registry. These are reported as probable, never certain.
  • Source scanning has no resolved compilation. Tier 2 findings come from syntax alone. For extra confidence you can also scan compiled binaries (Tier 3, via migrationscan YourApp.dll), which reads referenced assemblies from the assembly metadata.
  • Effort figures are heuristic. They are planning aids derived from static analysis, not a quote.
  • The reference inventory is what projects declare. Transitive package dependencies, <Import>ed build targets, and binding redirects are not collected — see what isn't collected. Resolving the full package graph would need a restore, and therefore the network.
  • Architectural decisions are yours. MigrationScan flags what blocks a migration; it does not decide how to redesign around it.

Building from source

Requires the .NET 10 SDK.

git clone <repo-url>
cd MigrationScan
dotnet build
dotnet test

Releases ship from a v* tag — see publishing. Changes are recorded in the changelog.

Roadmap

Development proceeds in ordered phases (see the spec for detail):

  • Phase 0 — Foundation: repo, license, CI on Linux/Windows/macOS, empty solution
  • Phase 1 — Walking skeleton: parse .sln/.csproj, first rule (MIG1001), console + JSON output
  • Phase 2 — Rule engine (project-file + Roslyn syntax rules) and the first rule batch
  • Phase 3 — Roslyn syntax rules (Tier 2): 12 runtime/blocking-framework detectors
  • Phase 4 — Effort model and Markdown report (golden-file tested)
  • Phase 5 — CI integration: SARIF, --fail-on exit codes, --baseline
  • Phase 6 — Post-v1: --online NuGet deprecation lookups, VB.NET support (projects + source), Mono.Cecil binary analysis, and an expanded rule catalog (28 rules). Further catalog rules land as needed.

Open questions

A few decisions from the spec are still open and will be resolved before v1:

  • VB.NET support — .vbproj projects are discovered and their .vb source is analyzed by the same rules as C#: the syntax queries are language-neutral, so VB gets the runtime-failure (Tier 2) rules too, honouring VB's case-insensitive matching.
  • Default target framework — pinned to net10.0 (LTS) for now; may float to whatever is current LTS.
  • Schema distribution — ship a --json-schema command vs. publish the schema as a static file.

License

Apache-2.0. The patent grant matters for enterprise legal review.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.1.6 139 7/28/2026
0.1.5 112 7/27/2026
0.1.4 112 7/27/2026
0.1.3 119 7/26/2026
0.1.2 120 7/26/2026
0.1.1 121 7/25/2026
0.1.0 118 7/25/2026