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.EnterpriseServicesand Windows Workflow Foundation are not available on .NET. Some APIs still exist but throw aPlatformNotSupportedExceptionat 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
PackageReferenceinstead ofpackages.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.
<#
.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
./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.
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];
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.
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
# 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
- Overview of upgrading .NET apps: paths of lifting, tools and GitHub Copilot modernization.
- Overview of porting from .NET Framework to .NET: Windows desktop technologies, Windows-specific APIs, unavailable technologies and helper tools.
- Analyze your dependencies to port code from .NET Framework to .NET:
packages.config, target versions of packages and compatibility mode. - .NET Framework technologies unavailable on .NET: AppDomains, remoting, Code Access Security and more.
- Platform compatibility analyzer: warnings for Windows-specific APIs.
- sys.sql_expression_dependencies: dependencies between database objects.
- dotnet package list: list the packages of an SDK project, detect outdated and vulnerable ones.