Public/Test-AssemblyProperty.ps1
|
function Test-AssemblyProperty { <# .SYNOPSIS Confirms whether one or more properties exist on a type inside a .NET assembly (e.g. the WSDL-generated proxy classes compiled into EndOfDay.exe), using reflection. .DESCRIPTION Loads the specified assembly for reflection and, for each requested property name, reports whether it is declared on the given type. This is useful for verifying that a service reference was regenerated and that newly added WSDL/XSD fields made it into the compiled proxy code. For each property it also reports the property type and whether the member carries a serialization attribute (DataMemberAttribute, XmlElementAttribute, XmlAttributeAttribute or SoapElementAttribute), which indicates the property is actually part of the serialized wire contract rather than just a plain CLR property. The assembly is loaded from a temporary copy so that a running application holding a lock on the original file does not block the check. Loading is done in a fresh child PowerShell process so repeated calls do not accumulate assemblies in the current session. Emits one object per requested property with these columns: Type, Property, Exists, PropertyType, Serialized, SerializationAttributes .PARAMETER AssemblyPath Path to the assembly (.exe or .dll) to inspect. .PARAMETER TypeName Full name of the type to examine (e.g. 'AMPServiceReference.ListTransmitItemsResponse'). If the namespace is omitted the first type whose simple name matches is used. If -TypeName is omitted entirely, every type in the assembly is examined (optionally scoped with -Namespace). .PARAMETER PropertyName One or more property names to check for on the type. If omitted, every property declared on the type is listed instead (each reported with Exists = True). .PARAMETER Namespace When -TypeName is omitted, restricts the examined types to those in this exact namespace. Defaults to 'AMPServiceReference'. Pass an empty string ('') to examine every type in the assembly. .PARAMETER CamelCase Converts any SCREAMING_SNAKE_CASE property names supplied via -PropertyName to camelCase before matching (e.g. 'UTC_TIMESTAMP_FORMAT' becomes 'utcTimestampFormat'). Names not in SCREAMING_SNAKE_CASE are left unchanged. .PARAMETER Static Also search static properties (instance + non-public are searched by default). .EXAMPLE Test-AssemblyProperty ` 'C:\dev\Progistics\trunk\source\Assemblies\EndOfDay.exe' ` 'AMPServiceReference.TransmitItem' ` 'sequence', 'newField' .EXAMPLE # List every property on the type Test-AssemblyProperty $exe 'AMPServiceReference.TransmitItem' | Format-Table Property, PropertyType, Serialized -AutoSize .EXAMPLE # List every property on every proxy type in a namespace Test-AssemblyProperty $exe -Namespace 'AMPServiceReference' | Format-Table Type, Property, PropertyType -AutoSize .EXAMPLE # Only report the properties that are missing Test-AssemblyProperty $exe 'AMPServiceReference.TransmitItem' $expected | Where-Object { -not $_.Exists } .EXAMPLE # Confirm the new fields are part of the serialized contract Test-AssemblyProperty $exe 'AMPServiceReference.TransmitItem' $expected | Format-Table Property, Exists, Serialized, SerializationAttributes -AutoSize #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [Alias('Path')] [string]$AssemblyPath, [Parameter(Mandatory = $false, Position = 1)] [Alias('Type')] [string]$TypeName, [Parameter(Mandatory = $false, Position = 2, ValueFromRemainingArguments = $true)] [Alias('Property', 'Properties')] [string[]]$PropertyName, [Parameter(Mandatory = $false)] [string]$Namespace = 'AMPServiceReference', [switch]$CamelCase, [switch]$Static ) $resolved = (Resolve-Path -LiteralPath $AssemblyPath -ErrorAction Stop).ProviderPath # Convert any SCREAMING_SNAKE_CASE property names supplied by the caller to # camelCase (e.g. 'UTC_TIMESTAMP_FORMAT' -> 'utcTimestampFormat') so callers # can pass constant-style names and still match the generated proxy members. if ($CamelCase -and $PropertyName) { $PropertyName = $PropertyName | ForEach-Object { if ($_ -cmatch '^[A-Z0-9]+(_[A-Z0-9]+)*$') { $segments = $_ -split '_' $camel = $segments[0].ToLowerInvariant() for ($i = 1; $i -lt $segments.Count; $i++) { $segment = $segments[$i] if ($segment.Length -gt 0) { $camel += $segment.Substring(0, 1).ToUpperInvariant() + $segment.Substring(1).ToLowerInvariant() } } $camel } else { $_ } } } # Run the reflection in a child process so we can load from a temp copy # (avoids file locks from a running app) without polluting the current # session with a permanently loaded assembly. $worker = { param($AssemblyPath, $TypeName, $PropertyNamesCsv, $NamespaceFilter, $IncludeStatic) $propertyNames = if ([string]::IsNullOrEmpty($PropertyNamesCsv)) { @() } else { $PropertyNamesCsv -split "`n" } $tempPath = [System.IO.Path]::Combine( [System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + [System.IO.Path]::GetExtension($AssemblyPath)) Copy-Item -LiteralPath $AssemblyPath -Destination $tempPath -Force try { $asm = [System.Reflection.Assembly]::LoadFrom($tempPath) # GetTypes() throws ReflectionTypeLoadException if any type fails to # load; recover the ones that did load so a single bad type does not # abort the whole listing. $getLoadableTypes = { param($assembly) try { $assembly.GetTypes() } catch [System.Reflection.ReflectionTypeLoadException] { $_.Exception.Types | Where-Object { $null -ne $_ } } } # Resolve the set of types to examine. When -TypeName is supplied we # look at just that type; otherwise we examine every type (optionally # scoped to -Namespace). $types = @() if (-not [string]::IsNullOrEmpty($TypeName)) { $type = $asm.GetType($TypeName, $false, $true) if ($null -eq $type) { $type = (& $getLoadableTypes $asm) | Where-Object { $_.Name -eq $TypeName -or $_.FullName -eq $TypeName } | Select-Object -First 1 } if ($null -eq $type) { throw "Type '$TypeName' was not found in '$AssemblyPath'." } $types = @($type) } else { $types = & $getLoadableTypes $asm if (-not [string]::IsNullOrEmpty($NamespaceFilter)) { $types = $types | Where-Object { $_.Namespace -eq $NamespaceFilter } } $types = $types | Sort-Object FullName } $flags = [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::Public -bor [System.Reflection.BindingFlags]::NonPublic if ($IncludeStatic) { $flags = $flags -bor [System.Reflection.BindingFlags]::Static } $serializationAttributeNames = @( 'DataMemberAttribute', 'XmlElementAttribute', 'XmlAttributeAttribute', 'SoapElementAttribute' ) # When no property names are supplied, list every property declared on # the type (sorted) instead of checking specific ones. $listAll = ($propertyNames.Count -eq 0) foreach ($type in $types) { # Build a name -> PropertyInfo map. Using GetProperties() and # grouping avoids GetProperty(name, flags) throwing an ambiguous # match exception on types with overloaded indexers (e.g. an # 'Item' property with multiple signatures). $allProps = $type.GetProperties($flags) $propMap = @{} foreach ($p in $allProps) { if (-not $propMap.ContainsKey($p.Name)) { $propMap[$p.Name] = $p } } $namesForType = if ($listAll) { $allProps | ForEach-Object { $_.Name } | Sort-Object -Unique } else { $propertyNames } foreach ($name in $namesForType) { $prop = $propMap[$name] $serializeAttrs = @() if ($null -ne $prop) { $serializeAttrs = $prop.GetCustomAttributes($false) | ForEach-Object { $_.GetType().Name } | Where-Object { $serializationAttributeNames -contains $_ } } [pscustomobject]@{ Type = $type.FullName Property = $name Exists = ($null -ne $prop) PropertyType = if ($prop) { $prop.PropertyType.FullName } else { $null } Serialized = ($serializeAttrs.Count -gt 0) SerializationAttributes = ($serializeAttrs -join ', ') } } } } finally { Remove-Item -LiteralPath $tempPath -Force -ErrorAction SilentlyContinue } } # Join names with newline so they survive the process boundary as a single arg. $namesCsv = ($PropertyName -join "`n") $pwsh = (Get-Process -Id $PID).Path & $pwsh -NoProfile -NonInteractive -Command $worker -args $resolved, $TypeName, $namesCsv, $Namespace, ([bool]$Static) Write-Verbose "Parameter values:" Write-Verbose " AssemblyPath : $resolved" Write-Verbose " TypeName : $(if ([string]::IsNullOrEmpty($TypeName)) { '<all types>' } else { $TypeName })" Write-Verbose " PropertyName : $(if ($PropertyName) { $PropertyName -join ', ' } else { '<all properties>' })" Write-Verbose " Namespace : $(if ([string]::IsNullOrEmpty($Namespace)) { '<all namespaces>' } else { $Namespace })" Write-Verbose " Static : $([bool]$Static)" } |