You have decided that the Order Book will be modernized. Before you change anything, you have to know what you are changing: which projects, which packages, which Windows peculiarities, which database objects, and above all who calls whom. The inventory is the work that later saves you hours, over and over, because nobody has to guess anymore.

An inventory is not an assessment by a tool. It delivers facts that you can check yourself, and a map that the team reads together. In this lesson you produce both, once by hand with a small script, and then place what Microsoft's tools contribute.

What an inventory captures

View Question How you find out
Projects Which projects are there, in which format, for which target version? read the project files
Packages Which packages in which versions, and do they exist for .NET? packages.config, later dotnet package list
Windows dependencies Which parts of the program depend on Windows (registry, printing, sign-in, browser control)? search the source code
Database Which tables, procedures and triggers are there, and who uses them? query the system views
Who calls whom How do the classes and forms hang together? reading, searching, a drawing
Environment Which servers, drives, groups, printers and accounts does the program use? configuration and talking to the people

The first four rows can be largely automated. The last two cannot: "Who calls whom" and "Environment" need people who know the program. Ms. Aydin knows that the export runs every Friday. In the source code that is written nowhere.

What Microsoft recommends

Microsoft's documentation on upgrading .NET programs names things to check before the rebuild. They are the framework for the inventory:

  • Dependencies first. Packages and libraries must target .NET, .NET Standard or .NET Core. A package that supports only .NET Framework is an obstacle or a case for compatibility mode (warning NU1701), which you have to test.
  • Windows-specific APIs. Many .NET Framework APIs do not exist on .NET because they are tied to Windows, such as the Windows registry or the GDI+ drawing model. The Windows Compatibility Pack (package Microsoft.Windows.Compatibility) provides a large part of them, according to Microsoft around 20,000 APIs, among others for the registry, event log, WMI, directory services and access control.
  • Technologies that no longer exist. Application domains (AppDomains), remoting, Code Access Security, Security Transparency, System.EnterpriseServices and Windows Workflow Foundation are not available on .NET. Some APIs still exist but throw a PlatformNotSupportedException at runtime. Microsoft therefore advises testing early.
  • Third-party controls. They may never have been released for .NET and then stay with .NET Framework.
  • Project format. The project should be in SDK format and use PackageReference instead of packages.config.

In addition there are tools that Microsoft describes as helpers. They do not replace the inventory, they speed it up:

Tool What it does Note
GitHub Copilot modernization assessment, planning and execution in three stages; assesses project structure, dependencies and code patterns; keeps its state under .github/upgrades/ recommended by Microsoft; requires GitHub Copilot
Azure Migrate: application and code assessment for .NET analysis of code and application with recommendations for cloud deployment Copilot modernization uses this tool
.NET Upgrade Assistant command-line tool for lifting described by Microsoft as deprecated, meant only for environments without Copilot
Platform compatibility analyzer (CA1416) warns when you call Windows-specific APIs without a matching platform reference on by default for targets from net5.0, can be switched on for other targets

This course shows every step by hand. A tool may take work off your hands, but you have to check what it did, and for that you have to know what it should have done. For the inventory that holds doubly: The answer "the tool found nothing" is worth something only if you know what it was looking for.

The script: projects, packages, dependencies

The script reads a folder of projects and prints three tables: the projects (format, target version, kind), the packages from packages.config and the hits for Windows dependencies. It searches with regular expressions in the *.cs and *.config files and states for each hit what it means for the migration. It only reads and changes nothing. The search patterns are a list that you can extend.

PowerShell
<#
.SYNOPSIS
Inventory of a .NET project tree: projects, packages and hits for Windows dependencies. Only reads, changes nothing.

.EXAMPLE
./scripts/Inventory.ps1 -Path ./Brandt.OrderBook
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)]
    [string] $Path
)

$ErrorActionPreference = 'Stop'
$Path = (Resolve-Path -LiteralPath $Path).Path
$excludeBuild = '[\\/](bin|obj|packages)[\\/]'

# Name -> pattern -> what it means for the migration
$patterns = [ordered]@{
    'Windows Forms'       = @{ Regex = 'System\.Windows\.Forms'; Consequence = 'net10.0-windows, UseWindowsForms' }
    'DataSet/ADO.NET'     = @{ Regex = 'SqlDataAdapter|DataSet|DataTable|SqlCommandBuilder'; Consequence = 'Replace the data access' }
    'App.config'          = @{ Regex = 'ConfigurationManager'; Consequence = 'appsettings.json, Configuration' }
    'Windows sign-in'     = @{ Regex = 'WindowsIdentity|WindowsPrincipal'; Consequence = 'Microsoft Entra ID' }
    'Printing/Drawing'    = @{ Regex = 'PrintDocument|PrintPreviewDialog|System\.Drawing'; Consequence = 'System.Drawing.Common, PDF later' }
    'Browser control'     = @{ Regex = 'WebBrowser'; Consequence = 'WebView2' }
    'Registry'            = @{ Regex = 'Microsoft\.Win32|Registry\.'; Consequence = 'Windows Compatibility Pack' }
    'Event log'           = @{ Regex = 'EventLog'; Consequence = 'Windows Compatibility Pack' }
    'Network drive (UNC)' = @{ Regex = '\\\\[A-Za-z0-9._-]+\\'; Consequence = 'Blob storage or file share' }
    'Code pages'          = @{ Regex = 'Encoding\.GetEncoding\('; Consequence = 'Code pages provider needed' }
    'AppDomain/Remoting'  = @{ Regex = 'AppDomain\.CreateDomain|System\.Runtime\.Remoting|BinaryFormatter'; Consequence = 'Not available on .NET' }
    'Native calls'        = @{ Regex = 'DllImport'; Consequence = 'Check, Windows-specific' }
}

$projects = foreach ($file in Get-ChildItem -LiteralPath $Path -Filter '*.csproj' -Recurse -File | Where-Object FullName -NotMatch $excludeBuild) {
    $xml = [xml](Get-Content -LiteralPath $file.FullName -Raw)
    $sdk = $null -ne $xml.Project.Sdk
    $target = $xml.SelectNodes("//*[local-name()='TargetFramework' or local-name()='TargetFrameworkVersion' or local-name()='TargetFrameworks']") | Select-Object -First 1
    $kind = $xml.SelectNodes("//*[local-name()='OutputType']") | Select-Object -First 1
    $references = @($xml.SelectNodes("//*[local-name()='ProjectReference']") | ForEach-Object { $_.Include })
    [pscustomobject]@{
        Project    = $file.BaseName
        Format     = if ($sdk) { 'SDK' } else { 'classic' }
        Target     = if ($target) { $target.InnerText } else { '?' }
        Kind       = if ($kind) { $kind.InnerText } else { 'Library' }
        References = $references.Count
    }
}

$packages = foreach ($file in Get-ChildItem -LiteralPath $Path -Filter 'packages.config' -Recurse -File | Where-Object FullName -NotMatch $excludeBuild) {
    foreach ($package in ([xml](Get-Content -LiteralPath $file.FullName -Raw)).packages.package) {
        [pscustomobject]@{ Package = $package.id; Version = $package.version; Source = 'packages.config' }
    }
}

$sources = Get-ChildItem -LiteralPath $Path -Filter '*.cs' -Recurse -File | Where-Object FullName -NotMatch $excludeBuild
$configs = Get-ChildItem -LiteralPath $Path -Filter '*.config' -Recurse -File | Where-Object FullName -NotMatch $excludeBuild
$hits = foreach ($name in $patterns.Keys) {
    $found = @(($sources + $configs) | Select-String -Pattern $patterns[$name].Regex)
    if ($found.Count -gt 0) {
        [pscustomobject]@{
            Dependency  = $name
            Hits        = $found.Count
            Files       = ($found.Path | ForEach-Object { Split-Path $_ -Leaf } | Sort-Object -Unique) -join ', '
            Consequence = $patterns[$name].Consequence
        }
    }
}

'--- Projects'
$projects | Format-Table -AutoSize | Out-String -Width 200
'--- Packages'
$packages | Format-Table -AutoSize | Out-String -Width 200
'--- Windows dependencies'
$hits | Format-Table -AutoSize -Wrap | Out-String -Width 200
PowerShell
./scripts/Inventory.ps1 -Path ./Brandt.OrderBook
--- Projects

Project          Format  Target Kind   References
-------          ------  ------ ----   ----------
Brandt.OrderBook classic v4.8   WinExe          0

--- Packages

Package         Version Source
-------         ------- ------
Newtonsoft.Json 13.0.3  packages.config

--- Windows dependencies

Dependency          Hits Files                                                              Consequence
----------          ---- -----                                                              -----------
Windows Forms          4 CustomersForm.cs, MainForm.cs, MaintenanceForm.cs, Program.cs      net10.0-windows, UseWindowsForms
DataSet/ADO.NET       15 Exports.cs, MainForm.cs, OrderData.cs                              Replace the data access
App.config             2 Settings.cs                                                        appsettings.json, Configuration
Windows sign-in        2 Permissions.cs                                                     Microsoft Entra ID
Printing/Drawing       8 CustomersForm.cs, InvoicePrint.cs, MainForm.cs, MaintenanceForm.cs System.Drawing.Common, PDF later
Browser control        2 CustomersForm.cs                                                   WebView2
Registry               3 MainForm.cs                                                        Windows Compatibility Pack
Event log              1 Program.cs                                                         Windows Compatibility Pack
Network drive (UNC)    1 App.config                                                         Blob storage or file share
Code pages             1 Exports.cs                                                         Code pages provider needed

The picture is clear. The program is one project in the classic format, it has one package, and it depends on Windows Forms, the DataSet, App.config, Windows sign-in, printing, the browser control, the registry, the event log and a network drive. The "Consequence" column is a suggestion, not a finding: It assigns each dependency to a lesson of this course, and you check every entry yourself.

The database

The database tells you what the programmers never wrote down. Two queries are enough: the objects and the dependencies between them. The view sys.sql_expression_dependencies holds one row per dependency from object to object. Stored procedures and triggers appear there as dependent objects.

SQL
SET NOCOUNT ON;

SELECT o.type_desc AS Kind, o.name AS Name, ISNULL(p.name, N'') AS BelongsTo
FROM sys.objects AS o
LEFT JOIN sys.objects AS p ON p.object_id = o.parent_object_id AND o.type = N'TR'
WHERE o.is_ms_shipped = 0 AND o.type IN (N'U', N'V', N'P', N'FN', N'TR')
ORDER BY o.type_desc, o.name;

SELECT OBJECT_NAME(d.referencing_id) AS [From], d.referenced_entity_name AS [To]
FROM sys.sql_expression_dependencies AS d
WHERE d.referencing_id IN (SELECT object_id FROM sys.objects WHERE is_ms_shipped = 0)
GROUP BY OBJECT_NAME(d.referencing_id), d.referenced_entity_name
ORDER BY [From], [To];
PowerShell
sqlcmd -S "(localdb)\MSSQLLocalDB" -E -d OrderBook -b -i scripts/Inventory-Database.sql -W
Kind Name BelongsTo
---- ---- ---------
SQL_STORED_PROCEDURE MaintenanceDue 
SQL_TRIGGER trg_Orders_MaintenanceDone Orders
USER_TABLE Customers 
USER_TABLE MaintenancePlan 
USER_TABLE Orders 
From To
---- --
CK_MaintenancePlan_Interval MaintenancePlan
CK_Orders_Kind Orders
CK_Orders_Status Orders
MaintenanceDue Customers
MaintenanceDue MaintenancePlan
trg_Orders_MaintenanceDone deleted
trg_Orders_MaintenanceDone inserted
trg_Orders_MaintenanceDone MaintenancePlan

Two things stand out. The trigger trg_Orders_MaintenanceDone hangs on Orders and changes MaintenancePlan, and no part of the program calls it: It runs on its own when a row changes. Whoever later moves the Orders table into another database or into another program takes this rule along unnoticed or loses it. And the view knows only dependencies inside the database: How the program calls the procedure MaintenanceDue is not in there.

What the scripts do not see

Every tool searches for what you tell it. The rest stays invisible:

Break point Why the script does not find it
B5 price rules in the form They are ordinary C# methods. No pattern reveals that they are business rules.
B6 duplicate rule "next maintenance" An AddMonths line looks harmless. That the procedure does the same thing is somewhere else.
B10 hard-coded font and sizes The script does not search for font sizes.
B11 global state Program.Data is a property name, not an error.
Who operates what, when Lives in heads, not in the code.

That is not a flaw of the script but the nature of an inventory from source code. That is why the conversation is part of it: Ask Ms. Aydin and Mr. Kowalski when they use what, and what they do when the program does not run. Write down what you hear.

The map

The map shows how the program hangs together and where it touches the outside world. A diagram saves pages of text. It belongs in docs/map.md and is drawn as Mermaid so that it is visible in the repository and in review. Arrows mean "calls" or "reads". The outside world is on the right.

mermaid
flowchart LR
    subgraph Application["Brandt.OrderBook.exe"]
        P[Program]
        H[MainForm]
        K[CustomersForm]
        W[MaintenanceForm]
        D[OrderData]
        R[InvoicePrint]
        X[Exports]
        E[Settings]
        B[Permissions]
    end
    P --> D
    P --> H
    H --> K
    H --> W
    H --> R
    H --> X
    H --> D
    H --> B
    K --> D
    W --> D
    W --> X
    R -->|Price rules| H
    X -->|Price rules| H
    D --> E
    X --> E
    B --> E
    D --> SQL[("SQL Server: tables, procedure, trigger")]
    X --> NL["Network drive Accounting"]
    H --> REG["User's registry"]
    P --> EL["Event log"]
    B --> WIN["Windows sign-in and groups"]
    K --> WB["Browser control"]
    R --> DR["Printer"]

Read the diagram for anything unusual. Two arrows stand out: InvoicePrint and Exports call the main form to calculate prices. A form is the source of business rules, and the two other classes depend on it. That is a seam that you will look for in the next lesson. And OrderData hangs on almost everything: a dense node that a lot will hang on later.

The rating

Every dependency gets a rating on three levels: low (a package or a setting is enough), medium (a replacement technology and some rebuilding) and high (the design changes). The levels are a judgment of the team, not a measurement. Write next to each one how you know: from Microsoft's documentation, from a trial or from experience.

Dependency Level Justification
Windows Forms low Windows Forms exists on .NET, only on Windows (documentation); the project needs the target net10.0-windows.
App.config low App.config keeps working in .NET through the package System.Configuration.ConfigurationManager (documentation); the move to appsettings.json is a later step.
Registry, event log low The Windows Compatibility Pack covers both (documentation).
Browser control medium For the WPF control, the documentation names the replacement WebView2; for Windows Forms it has to be checked in lesson M2.3.
Printing, drawing medium to be checked in lesson M2.3; later a PDF generator replaces the paper.
Network drive medium not available on the web; the replacement is storage in the cloud (lesson M4.5).
DataSet and adapter high sits under every form; is pulled out together with the logic (lesson M3.2).
Windows sign-in high does not exist like that on the web; the replacement is Microsoft Entra ID (module M5).
Price rules in the form high have to be pulled out first, before anything else uses them (lesson M3.1).
Trigger and procedure high invisible to the program; have to be considered together with the data (lesson M3.3).

What can go wrong

  • The script is taken as proof. "The script found nothing about remoting" means only that the patterns found nothing. Read the patterns before you trust the result.
  • Everything equally important. A map with 40 boxes and no rating helps nobody. Rate, and leave out what is unimportant.
  • Only the source code. The Friday export, the dispatch group and the network drive are in the environment, not in the code.
  • The levels become numbers. "Medium" stays a judgment. Whoever converts it into days invents a precision that does not exist.
  • The database is overlooked. A trigger that no program calls is the rule that gets lost first.
  • The tool replaces the check. The assessment of Copilot modernization is a suggestion, too. Compare it with your map and clarify every difference.

Exercise 1

Create scripts/Inventory.ps1 and scripts/Inventory-Database.sql in your repository and run both against your Order Book. Save the outputs in docs/inventory.txt. Then compare the script's hits with the twelve break points from lesson M1.2: Which ones does the script find, which not?

How to check your result: You have a list with two columns, "found by the script" and "found only by reading", and for every break point in the second column it says why the script does not see it.

Solution 1

The outputs look as shown above. The comparison:

Break point Found by the script? Why
B1 classic project yes column "Format: classic" and package from packages.config
B2 App.config yes pattern ConfigurationManager
B3 Windows sign-in yes pattern WindowsIdentity
B4 DataSet yes pattern SqlDataAdapter, DataSet, DataTable
B5 price rules in the form no ordinary methods, no pattern
B6 duplicate rule no, only the database objects the procedure and the trigger appear in the query; that Exports contains the same rule does not
B7 printing yes pattern PrintDocument, System.Drawing
B8 network drive, encoding yes pattern for UNC paths and Encoding.GetEncoding(
B9 browser control yes pattern WebBrowser
B10 hard-coded font no no pattern for it
B11 global state no Program.Data is an ordinary name
B12 registry, event log yes pattern Microsoft.Win32, Registry. and EventLog

So the script finds eight of the twelve break points, and exactly the four it does not find are the ones that take the most work in a modernization: rules and state. That is the most important insight of the inventory.

Exercise 2

Write docs/map.md. It contains: (1) a summary in three sentences, (2) the projects and packages as a table, (3) the map as a Mermaid diagram, (4) the rating of the dependencies as a table with level and justification, (5) the database objects and (6) open questions for Ms. Aydin and Mr. Kowalski.

How to check your result: Every dependency from the script is in the rating, every level has a justification, and among the open questions there is at least one that only a person can answer.

Solution 2

markdown
# Map: Brandt.OrderBook

## Summary

The Order Book is a Windows Forms project in the classic format on .NET Framework 4.8 with one package (Newtonsoft.Json). It accesses
three tables, a procedure and a trigger directly with a DataSet. The script finds ten dependencies; the
business rules live in a form and partly a second time in the database.

## Projects and packages

| Project | Format | Target | Kind |
| --- | --- | --- | --- |
| Brandt.OrderBook | classic | v4.8 | WinExe |

| Package | Version |
| --- | --- |
| Newtonsoft.Json | 13.0.3 |

## Map

(the Mermaid diagram from the lesson)

## Rating

(the table from the lesson, with level and justification)

## Database

Tables Customers, Orders, MaintenancePlan; procedure MaintenanceDue; trigger trg_Orders_MaintenanceDone (advances the maintenance plan,
is called by no part of the program).

## Open questions

- Ms. Aydin: Which pricing rules apply besides the four in the form? Are there special prices for individual customers?
- Mr. Kowalski: Who reads the maintenance plan's JSON on the tablet, and what happens if the file is missing?
- Mr. Brandt: Does the export run on a schedule or on request, and who checks it?

The map is a work in progress. It gets more precise with every lesson; seams and order follow in the next one.

What you can do now

  • You capture projects, packages, Windows dependencies and database objects with a script and a query.
  • You know what such tools do not find, and you ask the people who operate the program.
  • You place Microsoft's helper tools and check their results.
  • You draw a map and rate every dependency with a justification.

In the next lesson you look for the seams in the map: the places where the program can be separated.

Further reading