MigrationScan.Tool 0.1.6

dotnet tool install --global MigrationScan.Tool --version 0.1.6
                    
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.6
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=MigrationScan.Tool&version=0.1.6
                    
nuke :add-package MigrationScan.Tool --version 0.1.6
                    

MigrationScan

Find out what it costs to move a .NET Framework solution to modern .NET.

MigrationScan tells you what blocks the migration, how much work each item is, and how far to trust each answer. It runs with no network, no account, and no source code leaving your machine.

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

migrationscan path/to/YourSolution.sln

You get one file, migrationscan-report.json, plus a summary on screen. No .NET install, no admin rights, no flags to learn. Run it with no arguments and it scans the current directory, so you can drop the executable into a repository root and double-click it.

The summary is the part you read first:

MigrationScan · target net10.0

Projects scanned    3
Findings            23   blocker 3 · high 7 · medium 11 · low 2
Estimated effort    29.3–88 engineer-days
Needs decision      1    architectural, left unpriced
Not assessed        1    Woodgrove.Database (.sqlproj)
Third-party         6    distinct references, inventory only
Staying on Windows  17 findings · 23.8–72 days · 6 fewer, all in one project

One scan prices both futures. The last row is the same findings read from the other stance, so you can see what staying on Windows saves before anyone argues about it. Every finding follows underneath, with its tier, effort band, location and remediation.

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

Downloads are signed. MW Creative LLC holds the Windows certificate, so that is the name in the SmartScreen prompt. The macOS downloads carry Matthew Williams, who owns the company, until the Apple developer account converts to an organization at renewal in early 2027. macOS ships as a .pkg that installs migrationscan into /usr/local/bin, because Apple staples a notarization ticket to a package and not to a bare executable, and a stapled ticket means Gatekeeper clears the tool without calling home. Every asset comes with a .sha256 file, signed or not, to check your download against.

Status: early release. 33 rules across 8 categories, both portability targets, and a redacted report format. Expect the rule catalog to keep growing. See the roadmap.

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

📊 Or the same report over a real eleven-project estate →, Microsoft's archived eShopModernizing sample: 108 findings and 197 third-party references, read in 1.5 seconds.

Why this exists

Microsoft deprecated the .NET Upgrade Assistant and now points teams at the GitHub Copilot modernization agent instead. That agent needs a GitHub account with Copilot access, and it works by sending your code to a model service.

For plenty of teams in finance, healthcare, defense and government, routing source through a third party is not a budget question. It is a policy they cannot get an exception to. Those same teams hold the largest .NET Framework estates, which leaves them the least served by the tooling and the most in need of it.

MigrationScan is what you run when the answer has to stay inside the building.

The promise

  • Offline by default. The default path makes no network calls. You get network access only by passing --online, and only for NuGet package compatibility lookups.
  • No telemetry. Nothing phones home. No usage collection, no login.
  • Your code stays put. MigrationScan transmits your source nowhere.
  • Deterministic. Same input, same output, every run.
  • No AI in the analysis path. Findings come from static analysis.

Non-goals

MigrationScan will not:

  • Modify your source code
  • Perform the upgrade
  • Use an LLM anywhere in the analysis path
  • Phone home, collect telemetry, or require a login
  • Give you a binding cost estimate. Effort figures are planning aids, not a quote.
  • Require Visual Studio or MSBuild
  • Replace human judgment on architectural decisions

How it works

MigrationScan parses your .sln, .csproj and .vbproj files as XML, then reads your .cs and .vb source with Roslyn. It registers no MSBuild and needs no Visual Studio, so it behaves the same on Windows, Linux and macOS.

Every finding carries a confidence tier, because static analysis can prove some things and only suspect others:

Tier Name Source
1 Certain Project files, packages.config, app.config, web.config, .sln. XML, no ambiguity.
2 Probable Roslyn syntax trees with no 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 your projects declare, with versions and where each one resolves from.

Entries carry no severity and no effort, and they never fail a build. They exist because the expensive unknowns in a migration usually belong to somebody else. A grid control from a vendor that folded, or a type library nobody ever built for 64-bit, will cost you weeks. Rules catch the ones matching a known pattern; the inventory hands you the rest of the list to research.

Third-party references (6 distinct), inventory only, not counted above:
  • nuget   Litware.Web.Controls 2018.2.611.40
  • nuget   Microsoft.AspNet.Mvc 5.2.9
  • nuget   Microsoft.AspNet.Web.Optimization 1.1.3
  • nuget   Proseware.Json 9.1.2  [3 projects]
  • dll     Litware.ChequeScanner 3.0.0.0
  • com     FabrikamImagingLib 3.0
  (Also read, not listed: 11 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 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, and --help is the authority. --fail-on looks only at the stance --target names, so carrying the second stance in the report never moves 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 still targets Windows through net10.0-windows, where COM interop, P/Invoke to Win32, the Registry and WMI keep working. Those APIs are Windows lock-in. They cost you something only if you also need to leave Windows.

You never have to choose, and you never 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 gap between those two numbers is what portability costs you, and one scan produces it. The target changes what a finding costs, never what the scan found, so the second stance is an exact re-evaluation of the same analysis.

Your console and Markdown reports show the stance --target names, cross-platform by default. There, Windows lock-in findings drop out of the severity counts, the effort estimate and --fail-on. They still appear under a "satisfied by target" section, flagged satisfiedByTarget in the JSON and suppressed in SARIF, so nothing hides from you. Findings that break everywhere keep full severity either way: WebForms, BinaryFormatter, Remoting, MVC 5 and the rest.

--target picks the .NET version, net8.0 against net10.0. The platform axis always reports both ways. To read the console summary from the Windows stance:

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

The JSON comes out 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. Pass --online and MigrationScan will ask nuget.org about package status. Today that means flagging packages the maintainers marked deprecated (MIG2002):

migrationscan . --online

Because these findings reflect live nuget.org state, they are not part of the deterministic default path. When a lookup fails, because you are offline or rate-limited, the scan prints a warning and carries on without package status instead of failing.

Scanning compiled binaries

Sometimes you have no source: a third-party component, or an early look at a client's build output. Point MigrationScan at the compiled assembly instead:

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, …). Those come back as Tier 3, Verified, read from compiled metadata rather than guessed 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 then 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 for 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 stay visible. 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 planning aids rather than a quote, so apply your own rates and judgment downstream.

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

The JSON report holds no source file paths. Every .cs and .vb location becomes a stable opaque id, so you can clear the file without anyone reading several thousand lines of JSON. Every run says so on screen, and every Markdown report ends with a "What this report contains" section. Project files are the deliberate exception: a report keeps the repo-relative path of each .csproj, because that path is how a project is identified and grouping by project is what makes a proposal readable downstream.

It includes: project paths, line numbers, rule identifiers with their remediation text, and the names and versions of the dependencies your projects declare. That covers NuGet packages, referenced assemblies, COM components, web-service endpoints and the Windows system libraries you call through P/Invoke. A name identifies a component; it says nothing about where it sits on disk. We keep names because nobody can assess a component without knowing which one it is.

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

Redaction covers the JSON, which is the file you send on. Your console, Markdown report and SARIF output keep full paths. They stay on your machine, SARIF exists to point at a line in a file, and hiding paths from your own developers would protect nobody. Add --include-paths if you want the JSON to keep them too.

Two details worth knowing. A fileId stays stable, so you can still see that two findings share a file, which helps when sizing work and discloses nothing. And a redacted report still works as a --baseline, because each finding records its own fingerprint instead of deriving one from the path.

Each report also records what produced it: the tool version, plus the commit you had checked out if you scanned 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, every project, one report:

migrationscan C:\code\LegacyEstate

Projects are the unit of truth here; solutions just group them. MigrationScan assesses a project because it exists on disk, so it still scans a project no solution references. Those are the ones that surface halfway through a migration and wreck the plan. You get them in the warnings, so you can decide whether they belong in scope. A project shared by several solutions gets scanned once.

MigrationScan skips build output, restored packages/, node_modules and dot-directories, so it never mistakes a vendored source tree for your own code.

Limitations

Static analysis without resolved references cannot see everything, so here is where MigrationScan stops short:

  • Tier 2 findings can be false positives. A reference to a type named Registry might be your own class rather than Microsoft.Win32.Registry. MigrationScan reports these as probable and never claims certainty.
  • Source scanning has no resolved compilation. Tier 2 findings come from syntax alone. Scan a compiled binary with migrationscan YourApp.dll for Tier 3 confidence, which reads referenced assemblies out of the assembly metadata.
  • Effort figures are heuristic. Treat them as planning aids, not a quote.
  • The reference inventory covers what your projects declare. It leaves out transitive package dependencies, <Import>ed build targets and binding redirects. See what isn't collected. Resolving the full package graph would need a restore, and a restore needs the network.
  • Architectural decisions stay yours. MigrationScan flags what blocks a migration. It will not tell you 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 for how, and the changelog for what changed.

Roadmap

Development runs in ordered phases. The spec has the detail.

  • Phase 0. Foundation: repo, license, CI on Linux, Windows and macOS
  • Phase 1. Walking skeleton: parse .sln/.csproj, first rule, console and JSON output
  • Phase 2. Rule engine for project-file and Roslyn syntax rules, plus the first rule batch
  • Phase 3. Roslyn syntax rules at Tier 2: 12 runtime and 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. --online NuGet deprecation lookups, VB.NET support, Mono.Cecil binary analysis, expanded rule catalog
  • Phase 7. Redaction: the shared JSON carries opaque ids instead of paths (schema 1.6)

Next up is a wider rule catalog. Open an issue if a pattern in your estate goes unflagged.

Design decisions worth knowing

  • VB.NET gets the same treatment as C#. MigrationScan discovers .vbproj projects and reads .vb source. The syntax queries are language-neutral, so VB picks up the Tier 2 runtime rules too, with VB's case-insensitive matching honoured.
  • The default target stays pinned at net10.0 and moves only in a release you can see. A floating default would mean two versions of the tool disagreeing about your codebase for a reason the report never mentions, which would break the determinism promise everything else rests on.
  • The schema ships as a file, not a command. docs/schema carries a JSON Schema per minor version, validated against real output in CI. You can link to it, reference it from a $schema key, and generate against it.

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 135 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