Showing posts with label Powershell. Show all posts
Showing posts with label Powershell. Show all posts

Sunday, 7 September 2025

Show Nuget Dependency Graph

Showing Nuget Dependency Graph

In .NET Framework and .NET solutions, Nugets are added to solutions to conveniently add libraries. Each project got possibly multiple Nuget package dependencies. Each Nuget package itself can reference other Nuget libraries, which again references again additional libraries and so on. An overview of all the Nuget libraries actually used by a project, those that can be called top-level and transitive dependencies. Transitive dependencies are those indirectly references by the top-level dependencies. Do not confused this with those libraries that are actually referenced in the project file (.csproj files for example) as a Package Reference directly with those called top-level dependencies in this article, top-level here means the Nuget is has got a dependency graph level depth of one, compared to transitive dependencies where the dependency graph level higher than one.



function Show-NugetDependencyGraph {
    [CmdletBinding()]
    param ()
    $tempHtmlPath = [System.IO.Path]::GetTempFileName() + ".html"
    $assetFiles = Get-ChildItem -Recurse -Filter "project.assets.json"

    $currentProjects = (gci -recurse -filter *.csproj | select-object -expandproperty name) -join ',' #get folder name to show

    $script:mermaidGraph = @"
<!DOCTYPE html>
<html>
<head>
  <script type="module">
    import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
    mermaid.initialize({ startOnLoad: true });
  </script>
  <style>
    body {
      font-family: sans-serif;
      padding: 20px;
    }
    .mermaid {
      background: #f9f9f9;
      padding: 20px;
      border-radius: 8px;
      min-height: 800px;
      overflow: hidden;
    }
    .mermaid svg {
      min-height: 800px;
      width: 100%;
      height: auto;
    }
  </style>
</head>

<meta charset="UTF-8">

<body>
<h2>Nuget Dependency Graph for '$currentProjects' (Max Depth: 3)</h2>
<div class="mermaid">


graph TD


"@
    $visited = @{}
    $nodes = @{}
    $edges = @{}
    $topLevelDeps = @{}
    $transitiveDeps = @{}
    function Escape-MermaidLabel {
        param ([string]$text)
        $text = $text -replace '\(', '('
        $text = $text -replace '\)', ')'
        $text = $text -replace '\[', '['
        $text = $text -replace '\]', ']'
        $text = $text -replace ',', ','
        return $text
    }
    function Normalize-NodeId {
        param ([string]$text)
        return ($text -replace '[^a-zA-Z0-9_]', '_')
    }
    function Add-Dependencies {
        param (
            [string]$pkgName,
            [object]$targets,
            [int]$depth,
            [string]$path = ""
        )
        if ($depth -gt 3 -or $visited.ContainsKey($pkgName)) { return }
        $visited[$pkgName] = $true
        $pkgVersion = $pkgName.Split('/')[1]
        $pkgId = $pkgName.Split('/')[0]
        $escapedVersion = Escape-MermaidLabel($pkgVersion)
        $nodeId = Normalize-NodeId ("{0}_{1}" -f $pkgId, $pkgVersion)
        $nodeLabel = "$nodeId[""$pkgId<br/>v$escapedVersion""]:::level$depth"
        if (-not $nodes.ContainsKey($nodeId)) {
            $script:mermaidGraph += "$nodeLabel`n"
            $nodes[$nodeId] = $true
        }
        $currentPath = if ($path) { "$path → $pkgId ($pkgVersion)" } else { "$pkgId ($pkgVersion)" }
        if ($depth -eq 1) {
            $topLevelDeps["$pkgId/$pkgVersion"] = $currentPath
        } else {
            $transitiveDeps["$pkgId/$pkgVersion"] = $currentPath
        }
        foreach ($target in $targets.PSObject.Properties) {
            $pkg = $target.Value.$pkgName
            if ($pkg -and $pkg.dependencies) {
                foreach ($dep in $pkg.dependencies.PSObject.Properties) {
                    $depName = $dep.Name
                    $depVersion = $dep.Value
                    $escapedDepVersion = Escape-MermaidLabel($depVersion)
                    $depNodeId = Normalize-NodeId ("{0}_{1}" -f $depName, $depVersion)
                    $depNodeLabel = "$depNodeId[""$depName<br/>v$escapedDepVersion""]:::level$($depth+1)"
                    if (-not $nodes.ContainsKey($depNodeId)) {
                        $script:mermaidGraph += "$depNodeLabel`n"
                        $nodes[$depNodeId] = $true
                    }
                    $edge = "$nodeId --> $depNodeId"
                    if (-not $edges.ContainsKey($edge)) {
                        $script:mermaidGraph += "$edge`n"
                        $edges[$edge] = $true
                    }
                    Add-Dependencies ("$depName/$depVersion") $targets ($depth + 1) $currentPath
                }
            }
        }
    }
    foreach ($file in $assetFiles) {
        $json = Get-Content $file.FullName | ConvertFrom-Json
        $targets = $json.targets
        foreach ($target in $targets.PSObject.Properties) {
            $targetPackages = $target.Value
            foreach ($package in $targetPackages.PSObject.Properties) {
                Add-Dependencies $package.Name $targets 1
            }
        }
    }

    $topLevelDepsCount = $topLevelDeps.Count #number of top level dependencies
    $transitiveDepsCount = $transitiveDeps.Count #number of top level transitive dependencies

    $script:mermaidGraph += @"
classDef level1 fill:#cce5ff,stroke:#004085,stroke-width:2px;
classDef level2 fill:#d4edda,stroke:#155724,stroke-width:1.5px;
classDef level3 fill:#fff3cd,stroke:#856404,stroke-width:1px;
</div>
<script>
  function enablePanZoom(svg) {
    let isPanning = false;
    let startX, startY;
    let viewBox = svg.viewBox.baseVal;
    let zoomFactor = 1.1;
    // Initial zoom: scale to 200%
    const initialZoom = 2.0;
    const newWidth = viewBox.width / initialZoom;
    const newHeight = viewBox.height / initialZoom;
    viewBox.x += (viewBox.width - newWidth) / 2;
    viewBox.y += (viewBox.height - newHeight) / 2;
    viewBox.width = newWidth;
    viewBox.height = newHeight;
    svg.addEventListener("mousedown", (e) => {
      isPanning = true;
      startX = e.clientX;
      startY = e.clientY;
      svg.style.cursor = "grabbing";
    });
    svg.addEventListener("mousemove", (e) => {
      if (!isPanning) return;
      const dx = (e.clientX - startX) * (viewBox.width / svg.clientWidth);
      const dy = (e.clientY - startY) * (viewBox.height / svg.clientHeight);
      viewBox.x -= dx;
      viewBox.y -= dy;
      startX = e.clientX;
      startY = e.clientY;
    });
    svg.addEventListener("mouseup", () => {
      isPanning = false;
      svg.style.cursor = "grab";
    });
    svg.addEventListener("mouseleave", () => {
      isPanning = false;
      svg.style.cursor = "grab";
    });
    svg.addEventListener("wheel", (e) => {
      e.preventDefault();
      const { x, y, width, height } = viewBox;
      const mx = e.offsetX / svg.clientWidth;
      const my = e.offsetY / svg.clientHeight;
      const zoom = e.deltaY < 0 ? 1 / zoomFactor : zoomFactor;
      const newWidth = width * zoom;
      const newHeight = height * zoom;
      viewBox.x += (width - newWidth) * mx;
      viewBox.y += (height - newHeight) * my;
      viewBox.width = newWidth;
      viewBox.height = newHeight;
    });
    svg.style.cursor = "grab";
  }
  document.addEventListener("DOMContentLoaded", () => {
    setTimeout(() => {
      const svg = document.querySelector(".mermaid svg");
      if (svg) {
        enablePanZoom(svg);
      } else {
        console.warn("SVG not found after 1.5s.");
      }
    }, 1500);
  });
</script>
<h3>πŸ”Ž Filter Dependencies (Total Count: $($transitiveDepsCount + $topLevelDepsCount))</h3>
<input type="text" id="searchInput" onkeyup="filterTables()" placeholder="Search for NuGet package..." style="width: 100%; padding: 8px; margin-bottom: 20px; font-size: 16px;">
<style>
  table {
    border-collapse: collapse;
    width: 100%;
    margin-bottom: 40px;
    font-size: 14px;
  }
  th, td {
    border: 1px solid #ccc;
    padding: 8px;
    text-align: left;
  }
  tr:nth-child(even) {
    background-color: #f9f9f9;
  }
  tr:hover {
    background-color: #e2f0fb;
  }
  th {
    background-color: #007bff;
    color: white;
  }
</style>
<h3>πŸ“¦ Top-Level Dependencies (Count: $transitiveDepsCount)</h3>
<em>Note: Top-level Dependencies are Nuget packages which have a Dependency Path of length 1. To check which Nuget packages are actually listed in the project file(s), open the .csproj file(s) directly.</em>
<table id="topTable">
  <thead><tr><th>Package</th><th>Dependency Path</th></tr></thead>
  <tbody>
"@
    $sortedTopLevel = $topLevelDeps.GetEnumerator() | Sort-Object Name
    foreach ($dep in $sortedTopLevel) {
        $script:mermaidGraph += "<tr><td>$($dep.Key)</td><td>$($dep.Value)</td></tr>`n"
    }
    $script:mermaidGraph += @"
  </tbody>
</table>
<h3>πŸ“š Transitive Dependencies (Count: $topLevelDepsCount)</h3>
<table id="transitiveTable">
  <thead><tr><th>Package</th><th>Dependency Path</th></tr></thead>
  <tbody>
"@
    $sortedTransitive = $transitiveDeps.GetEnumerator() | Sort-Object Name
    foreach ($dep in $sortedTransitive) {
        $script:mermaidGraph += "<tr><td>$($dep.Key)</td><td>$($dep.Value)</td></tr>`n"
    }
    $script:mermaidGraph += @"
  </tbody>
</table>
<script>
function filterTables() {
  const input = document.getElementById('searchInput').value.toLowerCase();
  ['topTable', 'transitiveTable'].forEach(id => {
    const rows = document.getElementById(id).getElementsByTagName('tr');
    for (let i = 1; i < rows.length; i++) {
      const cells = rows[i].getElementsByTagName('td');
      const match = Array.from(cells).some(cell => cell.textContent.toLowerCase().includes(input));
      rows[i].style.display = match ? '' : 'none';
    }
  });
}
</script>
</body>
</html>
"@
    [System.IO.File]::WriteAllText($tempHtmlPath, $script:mermaidGraph, [System.Text.Encoding]::UTF8)
    Start-Process $tempHtmlPath
}

# Run the function
Show-NugetDependencyGraph


The function above Show-NugetDependencyGraph can be added to the $profile file of the user you are logged in as. Usage : Make sure you are inside a folder where your project of the .NET Framework or .NET solution you want to see the Dependency graph and then just run the function Show-NugetDependencyGraph. Inside the subfolders, you will find project.assets.json file, usually in the obj folder. Note that this Powershell script do support showing multiple projects, but there are limitations in the graph drawing not allowing too many Nuget packages drawn into one and same graph, so the best analysis is done per-project. The Powershell script adds support for pan and zoom to provide an interactive Nuget Dependency graph. VanillaJs is used. Note that this script supports both .NET and .NET Framework. The script will recursively look for project.assets.json files in subfolders and then use the Convert-FromJson method to inspect the json file(s) found. The method Add-Dependencies is called recursively to build up the hash tables variables of the script that will keep the data structure that is keeping the list of Nuget libraries and transitive dependencies. The script also builds up VanillaJs script string that adds pan and zoom capabilities and the html template provides tables for the top-level and transitive Nuget libraries. Note also that the script builds up the html template that presents the Mermaid based Nuget dependency graph, using the script level variable $script:mermaidGraph. Note the usage of script-level variable here, this is necessary to hoist the Powershell variable up since we make use of recursion and this is required. Screenshots showing examples after running the Powershell script.

Table showing transitive dependencies in table :
Example of dependency graph of nuget libaries :

Friday, 27 December 2024

Terminating a process running on local port using Powershell

Developers who work with frontend and backend often switching between tools get a message that a certain process is holding a port. For example, using the dotnet command, you can get a message that the address is already in use. Note - you usually must decide if you really want to terminate the process running on a certain port, but if you are sure that you just want to close the process and free up the local port for its use, it would be nice to have a way of
just closing the process running on that local port. A typical output would be:

 System.IO.IOException: Failed to bind to address http://127.0.0.1:5000: address already in use.

We can locate which process is using te port, that is, the local port, and then terminate the process. This will free up the local port so we can use it. Here is a Powershell function that will find the process running at a local port, if any, given by a specied port number.


<#
.SYNOPSIS
Stops the process using the specified local port 

.DESCRIPTION
This function finds the process, if any, using the specified port and stops it.
In case there are no process, the function exits.

.PARAMETER portId
The port number to check for the process

.RETURNS int
If successful, returns 0. If not, returns -1.

.EXAMPLE
Terminate-Port -portId 5000
#>
function Terminate-Port {
    param (
        [Parameter(Mandatory = $true)]
        [int]$portId
    )

    # Get the process ID (PID) using the specified port 
    try {
        Write-Host "Looking for processing running using local port: $portId"
        $connection = Get-NetTCPConnection -LocalPort $portId -ErrorAction Stop
        if ($connection) {
            Write-Output "Port $portId is in use by process ID $($connection.OwningProcess | Select-Object -Unique)."
        } else {
            Write-Output "Port $portId is not in use."
            return 0 | Out-Null
        }
    } catch {
        Write-Output "An error occurred: $_"
        return -1 | Out-Null
    }

    $processId = $connection.OwningProcess

    if ($processId -and $processId -gt 0) {
        $process = Get-Process -Id $processId
        if ($process) {
            # Stop the process
            try {
                Stop-Process -Id $processId -Force
                Write-Host "Process running at port $portId was terminated."
                return 0 | Out-Null
            } catch {
                Write-Host "Could not stop process. Reason: $error"
            }
        }
    }

    Write-Host "No process found using port $portId."
    return -1 | Out-Null
}



To terminate , we just run the command

 Terminate-Port -portId 5000


Screenshots of the function being run: BEFORE the command Terminate-Port is run and AFTER. Note that the process running at the given portId was terminated and then the local port is freed up again.
AFTER

Wednesday, 10 June 2020

Creating a self signed certificate with Powershell and preparing it for IIS

I just wrote an automated routine in Powershell to create a self signed certificate.
#Install-Module -Name 'WebAdministration'

Import-Module -Name WebAdministration

function AddSelfSignedCertificateToSSL([String]$dnsname, [String]$siteName='Default Web Site'){
 $newCert = New-SelfSignedCertificate -DnsName $dnsname -CertStoreLocation Cert:\LocalMachine\My
 $binding = Get-WebBinding -Name $siteName -Protocol "https"
 $binding.AddSslCertificate($newCert.GetCertHashString(), "My")
 $newCertThumbprint = $newCert.Thumbprint
 $sourceCertificate = $('cert:\localmachine\my\' + $newCertThumbprint)
 
 $store = new-object system.security.cryptography.X509Certificates.X509Store -argumentlist "Root", LocalMachine
 $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]"ReadWrite")
 $store.Add($newCert)
 return $newCertThumbprint
}

Write-Host Installing self-signed certificate Cert:\LocalMachine\My and Cert:\LocalMachine\Root ..

$certinstalledThumbprint = AddSelfSignedCertificateToSSL 'someacmeapp.somedomain.net'

Write-Host Added certificate $certinstalledThumbprint to Cert:\LocalMachine\My and Cert:\LocalMachine\Root and set this up as the SSL certificate on Default Web Site.




Friday, 10 May 2019

Adding only untracked files in Git repo using Powershell

Are you a .NET developer mainly still use Windows OS and use Powershell and not Git bash for example ? The following procedure can be followed to create an aliased function for adding untracked files in a Git repository. Inside your $profile file of Powershell (in case it is missing - you can run: New-Item $Profile) notepad $Profile Now add this Powershell method:
function AddUntracked-Git() {
 &git ls-files -o --exclude-standard | select | foreach { git add $_ }
}
Save the $profile file and reload it into Powershell. Then reload your $profile file with: . $profile This is similar to the source command in *nix environments IMHO. So next time you, if you are developer using Powershell in Windows against Git repo and want to just include untracked files you can run: AddUntracked-Git This follows the Powershell convention where you have verb-nouns.

Friday, 3 May 2019

Powershell - Appending folder to path

The following Powershell function can be used to append a folder to the path for a user at the command line.

function AppendPath($filePath) {
 $path = [Environment]::GetEnvironmentVariable("Path")
 $path += ";" + $filePath
 [Environment]::SetEnvironmentVariable("Path", $path)
 Write-Host $path 
}

To call this function, just run the Powershell command:
AppendPath "c:\temp"
You can add this file into your $profile file as a function for easy availability. Run . $profile to reload your $profile file. This will append the folder "c:\temp" to your environment variable PATH. To view your environment variable PATH just enter:
echo $env:path
It is not required, but you can also use Chocolatey's refreshenv script to force update the environment variable if it is still not updating.

Tuesday, 26 February 2019

Powershell - starting and stopping multiple app pools

The following powershell script defines some functions in Powershell that can start up or stop all iis app pools on a server. It can be handy when you want to test out concurrency issues and switch off all IIS app pools and start up again.

Function fnStartApplicationPool([string]$appPoolName){
Import-Module WebAdministration 
 if ((Get-WebAppPoolState $appPoolName).Value -ne 'Started') {
  Write-Host 'IIS app pool ' $appPoolName ' is not started. Starting.' 
  Start-WebAppPool -Name $appPoolName 
  Write-Host 'IIS app pool ' $appPoolName 'started' 
 }
}

Function fnStartAllApplicationPools() {
Import-Module WebAdministration  
 Write-Host "Starting all app pools" 
 $appPools = (Get-ChildItem IIS:\AppPools)

foreach ($appPool in $appPools) { 
  & fnStartApplicationPool -appPoolName $appPool.Name
}
}

#fnStartAllApplicationPools #start all applications pools


Function fnStopApplicationPool([string]$poolname) {
Import-Module WebAdministration 
 if ((Get-WebAppPoolState $appPoolName).Value -ne 'Stopped') {
  Stop-WebAppPool -Name $appPoolName 
 }
}

Function fnStopAllApplicationPools(){
Import-Module WebAdministration  
 Write-Host "Starting all app pools" 
 $appPools = (Get-ChildItem IIS:\AppPools)

foreach ($appPool in $appPools) { 
  & fnStopApplicationPool-appPoolName $appPool.Name
}  

}

#fnStopAllApplicationPools #start all applications pools


Wednesday, 17 October 2018

Working with Netsh http sslcert setup and SSL bindings through Powershell

I am working with a solution at work where I need to enable IIS Client certificates. I am not able to get past the "Provide client certificate" dialog, but it is possible to alter the setup of SSL cert bindings on your computer through the Netsh command. This command is not in Powershell, but at the command line. I decided to write some Powershell functions to be able to alter this setup atleast in an easier way. One annoyance with the netsh command is that you have to keep track of the Application Id and Certificate hash values. Here, we can easier keep track of this through Powershell code. The Powershell code to display and alter, modify, delete and and SSL cert bindings is as follows:

function Get-NetshSetup($sslBinding='0.0.0.0:443') {

$sslsetup = netsh http show ssl 0.0.0.0:443
#Get-Member -InputObject $sslsetup

$sslsetupKeys = @{}

foreach ($line in $sslsetup){
 if ($line -ne $null -and $line.Contains(': ')){
    
    $key = $line.Split(':')[0]
    $value = $line.Split(':')[1]
     if (!$sslsetupKeys.ContainsKey($key)){
       $sslsetupKeys.Add($key.Trim(), $value.Trim()) 
      }
    } 
}


return $sslsetup

}

function Display-NetshSetup($sslBinding='0.0.0.0:443'){
 
 Write-Host SSL-Setup is: 

 $sslsetup = Get-NetshSetup($sslBinding)

foreach ($key in $sslsetup){
 Write-Host $key $sslsetup[$key]
}
}

function Modify-NetshSetup($sslBinding='0.0.0.0:443', $certstorename='My',
  $verifyclientcertrevocation='disable', $verifyrevocationwithcachedcleintcertonly='disable',
  $clientCertNegotiation='enable', $dsmapperUsage='enable'){
  $sslsetup = Get-NetshSetup($sslBinding)
 
  echo Deleting sslcert netsh http binding for $sslBinding ...
  netsh http delete sslcert ipport=$sslBinding
  echo Adding sslcert netsh http binding for $sslBinding...
  netsh http add sslcert ipport=$sslBinding certhash=$sslsetup['Certificate Hash'] appid=$sslsetup['Application ID'] certstorename=$certstorename verifyclientcertrevocation=$verifyclientcertrevocation verifyrevocationwithcachedclientcertonly=$verifyrevocationwithcachedcleintcertonly clientcertnegotiation=$clientCertNegotiation dsmapperusage=$dsmapperUsage
  echo Done. Inspect output.  
  Display-NetshSetup $sslBinding
}



function Add-NetshSetup($sslBinding, $certstorename, $certhash, $appid, 
  $verifyclientcertrevocation='disable', $verifyrevocationwithcachedcleintcertonly='disable',
  $clientCertNegotiation='enable', $dsmapperUsage='enable'){

  echo Adding sslcert netsh http binding for $sslBinding...
  netsh http add sslcert ipport=$sslBinding certhash=$certhash appid=$appid  clientcertnegotiation=$clientCertNegotiation dsmapperusage=$dsmapperUsage certstorename=$certstorename verifyclientcertrevocation=$verifyclientcertrevocation verifyrevocationwithcachedclientcertonly=$verifyrevocationwithcachedcleintcertonly 
   
  echo Done. Inspect output.  
  Display-NetshSetup $sslBinding
}





#Get-NetshSetup('0.0.0.0:443'); 
Display-NetshSetup
#Modify-NetshSetup 
Add-NetshSetup '0.0.0.0:443' 'MY' 'c0fe06da89bcb8f22da8c8cbdc97be413b964619' '{4dc3e181-e14b-4a21-b022-59fc669b0914}'
Display-NetshSetup