From 47a3549f4829d96c683c9016d959749439c718cb Mon Sep 17 00:00:00 2001 From: David Paulson Date: Fri, 20 Feb 2026 13:56:12 -0600 Subject: [PATCH] Convert Microsoft.IIs.PowerShell.Framework.* objects to PSCustom We need to this to help prevent serialization issues by having that class trying to convert it. Now it is done manually Add changes based off Copilot review --- .../IISInformation/ConvertTo-PSObject.ps1 | 122 ++++++++++++++++++ .../IISInformation/Get-IISWebApplication.ps1 | 48 +++++-- .../IISInformation/Get-IISWebSite.ps1 | 78 +++++++++-- 3 files changed, 221 insertions(+), 27 deletions(-) create mode 100644 Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/ConvertTo-PSObject.ps1 diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/ConvertTo-PSObject.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/ConvertTo-PSObject.ps1 new file mode 100644 index 0000000000..a0ab02ccdf --- /dev/null +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/ConvertTo-PSObject.ps1 @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +. $PSScriptRoot\..\..\..\..\..\Shared\ScriptBlockFunctions\RemotePipelineHandlerFunctions.ps1 + +<# +.DESCRIPTION + This function converts specific .NET or PowerShell objects into a custom PSCustomObject instance. + By copying selected properties onto a simple PSCustomObject, it helps avoid serialization + problems that can occur when remoting or persisting complex framework types. +#> +function ConvertTo-PSObject { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object]$ObjectToConvert, + + [Parameter(Mandatory = $true)] + [string[]]$ObjectTypeToConvert, + + [string[]]$PropertiesToSkip, + + [int]$DefaultDepthValue = 5 + ) + + begin { + Write-Verbose "Calling: $($MyInvocation.MyCommand)" + if ((Get-PSCallStack)[1].Command -ne "ConvertTo-PSObject") { + $Script:ConvertToPSObjectDepth = 0 + } + $newPSObject = New-Object PSCustomObject + } + process { + # The properties on the object that we want to pull out and place on a custom PS Object. + # This should help with serialization issues. + $objectGetTypeFullName = $ObjectToConvert.GetType().FullName + $doNotConvert = $true + + foreach ($type in $ObjectTypeToConvert) { + # This is intended to handle both ways that the wildcard can be setup and used. + if ($type -like $objectGetTypeFullName -or + $objectGetTypeFullName -like $type) { + $doNotConvert = $false + } + } + + # $Pester should only be set when we are doing Pester testing. + if ($doNotConvert -and + $Script:Pester -eq $false) { + Write-Verbose "Object '$($ObjectToConvert.GetType().FullName)' is not one of these types to convert: $([string]::Join(", ", $ObjectTypeToConvert))." + return + } + + $properties = ($ObjectToConvert | + Get-Member | + Where-Object { + $_.MemberType -ne "Method" -and + $_.MemberType -ne "ParameterizedProperty" -and + $_.MemberType -ne "CodeMethod" -and + $_.MemberType -ne "ScriptMethod" + }).Name | + Where-Object { $PropertiesToSkip -notcontains $_ } + Write-Verbose "Going to include the following properties to the object: $([string]::Join(", ", $properties))" + + foreach ($prop in $properties) { + # If the property is an array, we need to handle this differently. + # If the property is one of the types, Call this function again. Otherwise, add it as is. + if ($ObjectToConvert.$prop -is [array]) { + Write-Verbose "$prop is an Array on this object." + $list = New-Object System.Collections.Generic.List[object] + + foreach ($entry in $ObjectToConvert.$prop) { + if ($null -ne $entry) { + # Make this easier by just calling the method again, just need to add the type object that it is, just in case we are dealing with a list of a different type. + $Script:ConvertToPSObjectDepth++ + $value = $null + $params = @{ + ObjectToConvert = $entry + ObjectTypeToConvert = $ObjectTypeToConvert + ($entry.GetType().FullName) + PropertiesToSkip = $PropertiesToSkip + DefaultDepthValue = $DefaultDepthValue + } + ConvertTo-PSObject @params | Invoke-RemotePipelineHandler -Result ([ref]$value) + $list.Add($value) + } else { + # Add the empty list. + $list.Add($entry) + } + } + + $newPSObject | Add-Member -MemberType NoteProperty -Name $prop -Value $list + } elseif ($null -ne $ObjectToConvert.$prop -and + [string]::Empty -ne $ObjectToConvert.$prop -and + $null -ne ($ObjectTypeToConvert | Where-Object { $ObjectToConvert.$prop.GetType().FullName -like $_ })) { + + if ($Script:ConvertToPSObjectDepth -gt $DefaultDepthValue) { + Write-Verbose "Unable to convert this attribute property, as we are too deep in the object." + $newPSObject | Add-Member -MemberType NoteProperty -Name $prop -Value "--ERROR TOO DEEP--" + } else { + Write-Verbose "Going to call ConvertTo-PSObject for $prop property to expand" + $Script:ConvertToPSObjectDepth++ + $value = $null + $params = @{ + ObjectToConvert = ($ObjectToConvert.$prop) + ObjectTypeToConvert = $ObjectTypeToConvert + PropertiesToSkip = $PropertiesToSkip + DefaultDepthValue = $DefaultDepthValue + } + ConvertTo-PSObject @params | Invoke-RemotePipelineHandler -Result ([ref]$value) + $newPSObject | Add-Member -MemberType NoteProperty -Name $prop -Value $value + } + } else { + Write-Verbose "Adding property: $prop" + $newPSObject | Add-Member -MemberType NoteProperty -Name $prop -Value ($ObjectToConvert.$prop) + } + } + } + end { + $Script:ConvertToPSObjectDepth-- + return $newPSObject + } +} diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebApplication.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebApplication.ps1 index c2a6837e3a..3c33e40a02 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebApplication.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebApplication.ps1 @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +. $PSScriptRoot\ConvertTo-PSObject.ps1 + function Get-IISWebApplication { try { $webApplications = Get-WebApplication @@ -59,9 +61,29 @@ function Get-IISWebApplication { Write-Verbose "Failed to process additional context for: $($webApplication.ItemXPath). Exception: $($_.Exception)" } + # Convert the object to prevent serialization issues + $convertedObject = $null + $params = @{ + ObjectToConvert = $webApplication + ObjectTypeToConvert = "Microsoft.IIs.PowerShell.Framework*" + PropertiesToSkip = @("ChildElements", "Attributes", "Schema", "ConfigurationPathType", "ElementTagName", "Methods") + } + try { + ConvertTo-PSObject @params | Invoke-RemotePipelineHandler -Result ([ref]$convertedObject) + } catch { + Write-Verbose "Failed to convert the object. Inner Exception: $_" + Invoke-CatchActions + } + + if ($null -eq $convertedObject -and + $null -ne $webApplication) { + Write-Verbose "ConvertTo-PSObject failed to return an object. Using the default object." + $convertedObject = $webApplication + } + $returnList.Add([PSCustomObject]@{ FriendlyName = $friendlyName - Path = $webApplication.Path + Path = $convertedObject.Path ConfigurationFileInfo = ([PSCustomObject]@{ Valid = $validWebConfig Location = $configurationFilePath @@ -70,18 +92,18 @@ function Get-IISWebApplication { LinkedConfigurationLine = $linkedConfigurationLine LinkedConfigurationFilePath = $linkedConfigurationFilePath }) - ApplicationPool = $webApplication.applicationPool - EnabledProtocols = $webApplication.enabledProtocols - ServiceAutoStartEnabled = $webApplication.serviceAutoStartEnabled - ServiceAutoStartProvider = $webApplication.serviceAutoStartProvider - PreloadEnabled = $webApplication.preloadEnabled - PreviouslyEnabledProtocols = $webApplication.previouslyEnabledProtocols - ServiceAutoStartMode = $webApplication.serviceAutoStartMode - VirtualDirectoryDefaults = $webApplication.virtualDirectoryDefaults - Collection = $webApplication.Collection - Location = $webApplication.Location - ItemXPath = $webApplication.ItemXPath - PhysicalPath = $webApplication.PhysicalPath.Replace("%windir%", $env:windir).Replace("%SystemDrive%", $env:SystemDrive) + ApplicationPool = $convertedObject.applicationPool + EnabledProtocols = $convertedObject.enabledProtocols + ServiceAutoStartEnabled = $convertedObject.serviceAutoStartEnabled + ServiceAutoStartProvider = $convertedObject.serviceAutoStartProvider + PreloadEnabled = $convertedObject.preloadEnabled + PreviouslyEnabledProtocols = $convertedObject.previouslyEnabledProtocols + ServiceAutoStartMode = $convertedObject.serviceAutoStartMode + VirtualDirectoryDefaults = $convertedObject.virtualDirectoryDefaults + Collection = $convertedObject.Collection + Location = $convertedObject.Location + ItemXPath = $convertedObject.ItemXPath + PhysicalPath = $convertedObject.PhysicalPath.Replace("%windir%", $env:windir).Replace("%SystemDrive%", $env:SystemDrive) }) } diff --git a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebSite.ps1 b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebSite.ps1 index a63c0a835b..9fb6edf602 100644 --- a/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebSite.ps1 +++ b/Diagnostics/HealthChecker/DataCollection/ExchangeInformation/IISInformation/Get-IISWebSite.ps1 @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +. $PSScriptRoot\ConvertTo-PSObject.ps1 + function Get-IISWebSite { [CmdletBinding()] param() @@ -18,8 +20,35 @@ function Get-IISWebSite { foreach ($site in $webSites) { Write-Verbose "Working on Site: $($site.Name)" - $siteBindings = $bindings | + [array]$siteBindings = $bindings | Where-Object { $_.ItemXPath -like "*@name='$($site.name)' and @id='$($site.id)'*" } + + # Convert the object to prevent serialization issues + $siteBindingsConvertedList = New-Object System.Collections.Generic.List[object] + + foreach ($binding in $siteBindings) { + $convertedSiteBindings = $null + $params = @{ + ObjectToConvert = $binding + ObjectTypeToConvert = "Microsoft.IIs.PowerShell.Framework*" + PropertiesToSkip = @("ChildElements", "Attributes", "Schema", "ConfigurationPathType", "ElementTagName", "Methods") + } + + try { + ConvertTo-PSObject @params | Invoke-RemotePipelineHandler -Result ([ref]$convertedSiteBindings) + } catch { + Write-Verbose "Failed to convert the object. Inner Exception: $_" + Invoke-CatchActions + } + + if ($null -eq $convertedSiteBindings -and $null -ne $binding) { + Write-Verbose "ConvertTo-PSObject failed to return an object for Site Bindings. Using old object." + $convertedSiteBindings = $binding + } + + $siteBindingsConvertedList.Add($convertedSiteBindings) + } + # Logic should be consistent for all ways we call Get-WebConfigFile try { $configurationFilePath = (Get-WebConfigFile "IIS:\Sites\$($site.Name)").FullName @@ -134,23 +163,44 @@ function Get-IISWebSite { $physicalPath = $site.physicalPath.Replace("%windir%", $env:windir).Replace("%SystemDrive%", $env:SystemDrive) } + # Convert the object to prevent serialization issues + $convertedObject = $null + $params = @{ + ObjectToConvert = $site + ObjectTypeToConvert = "Microsoft.IIs.PowerShell.Framework*" + PropertiesToSkip = @("ChildElements", "Attributes", "Schema", "ConfigurationPathType", "ElementTagName", "Methods") + } + + try { + ConvertTo-PSObject @params | Invoke-RemotePipelineHandler -Result ([ref]$convertedObject) + } catch { + Write-Verbose "Failed to convert the object. Inner Exception: $_" + Invoke-CatchActions + } + + if ($null -eq $convertedObject -and + $null -ne $site) { + Write-Verbose "ConvertTo-PSObject failed to return an object. Using the default object." + $convertedObject = $site + } + $returnList.Add([PSCustomObject]@{ - Name = $site.Name - Id = $site.Id - State = $site.State - Bindings = $siteBindings - Limits = $site.Limits - LogFile = $site.logFile - TraceFailedRequestsLogging = $site.traceFailedRequestsLogging + Name = $convertedObject.Name + Id = $convertedObject.Id + State = $convertedObject.State + Bindings = $siteBindingsConvertedList + Limits = $convertedObject.Limits + LogFile = $convertedObject.logFile + TraceFailedRequestsLogging = $convertedObject.traceFailedRequestsLogging Hsts = [PSCustomObject]@{ - NativeHstsSettings = $site.hsts + NativeHstsSettings = $convertedObject.hsts HstsViaCustomHeader = $customHeaderHstsObj } - ApplicationDefaults = $site.applicationDefaults - VirtualDirectoryDefaults = $site.virtualDirectoryDefaults - Collection = $site.collection - ApplicationPool = $site.applicationPool - EnabledProtocols = $site.enabledProtocols + ApplicationDefaults = $convertedObject.applicationDefaults + VirtualDirectoryDefaults = $convertedObject.virtualDirectoryDefaults + Collection = $convertedObject.collection + ApplicationPool = $convertedObject.applicationPool + EnabledProtocols = $convertedObject.enabledProtocols PhysicalPath = $physicalPath ConfigurationFileInfo = [PSCustomObject]@{ Location = $configurationFilePath