Wissen/B04_Pipelining.ps1

# ? TITEL Pipelining
# ? DESCRIPTION Wie werden Pipeline-Objekte an Cmdlets gebunden
# ? TAGS Parameter Binding Splatting
# ? VERSION 2019.09.20.0800

[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
param()

#region Pipeline-Verarbeitung

# ! Nach einer Pipeline können Zeilenumbrüche eingefügt werden um die Lesbarkeit zu erhöhen
# ! Ab jetzt muss der auszuführende Block selektiert und dann mit F8 ausgeführt werden.
Get-Process |
    Where-Object -Property Company -like -Value "Microsoft*"  |
    Sort-Object Name | 
    Select-Object Name, Company |
    Out-File c:\temp\Process.txt -Force

# ! Windows PowerShell überträgt über die Pipeline KEINE TEXTE von Cmdlet A zu Cmdlet B.
# ! Es werden immer OBJEKT über die Pipeline übertragen.
Get-Process | Where-Object -Property Company -Match -Value "Microsoft"
Get-Process | ForEach-Object -Process {"Ist $_ ein Objekt? $($_ -is [System.Object])"}

# ! Cmdlet liefert Objekt immer i.d.R. just-in-time an das nächste Cmdlet
Get-ChildItem -Path c:\ -Recurse -ErrorAction SilentlyContinue | 
  ForEach-Object -Process { Write-Output "Empfange Objekt $_ und gebe $($_.FullName) weiter ..."; $_.FullName } | 
  Out-GridView

#endregion

#region Objekt-Bindung in der Pipeline

# ! Einführungsbeispiel
Get-Process -Name notepad | Stop-Process      # ? Würde dieser Befehl die notepad-Prozesse beenden? (JA | NEIN)
Get-ChildItem -Path c:\temp\DL | Stop-Process # ? Würde dieser Befehl die notepad-Prozesse beenden? (JA | NEIN)
# ! Eine Erklärung folgt weiter unten.

# ! 1. Das Pipeline-Objekt kann nur über die Parameter des Ziel-Cmdlet gebunden werden.
# ! 2. Für diese Bindung zwischen Pipeline-Objekt und den Parametern des Ziel-Cmdlets
# ! stehen ZWEI Verfahren zur Verfügung:
# ! A) ByValue Siehe Ziel-Cmdlet/Parameter-Beschreibung, bzw. was der Ziel-Cmdlet-Programmierer
# ! über ein Attribut 'ValueFromPipeline' konfiguriert hat.
# ! Ein Bindung des GANZEN PIPELINE-OBJEKTES findet statt,
# ! wenn der Pipeline-Objekt-TYP KOMPATIBEL (s.u.) mit diesem Parameter ist.
# ! B) ByPropertyName Siehe Ziel-Cmdlet/Parameter-Beschreibung, bzw. was der Ziel-Cmdlet-Programmierer
# ! über ein Attribut 'ValueFromPipelineByPropertyName' konfiguriert hat.
# ! Ein Bindung EINER EIGENSCHAFT des Pipeline-Objektes findet statt,
# ! wenn dieser Pipeline-Objekt-EigenschaftsNAME identisch ist mit dem Ziel-Cmdlet-ParameterNAME,
# ! oder dessen ALIASNAMEN UND der Pipeline-Objekt-EigenschaftsTYP KOMPATIBEL (s.u.)
# ! mit Ziel-Cmdlet-ParameterTYP.
# ! 3. Ein Mehrfach-Binden bzw. Mehrfach-Verfahren an das Ziel-Cmdlet ist möglich.

# ! Bzgl. Kompatibilität von Objekt-Typen: Objekte sind kompatibel wenn:
# ! A) Die Typennamen 100% identisch sind, was den Namespace mit einschließt
[System.String] -ceq [System.String]
[System.Timers.Timer] -ceq [System.Windows.Forms.Timer]
# ! B) Jedes Objekt ist kompatibel mit [System.Object]
"Hallo Köln!"    -is [System.Object]
(Get-Process)[0] -is [System.Object]
12               -is [System.Object]
# ! C) Jedes komplexe Objekt ist kompatibel mit [PSCustomObject] (Alias: PSObject)
(Get-Process)[0]   -is [PSCustomObject]
(Get-Service)[0]   -is [PSCustomObject]
(Get-ChildItem)[0] -is [PSCustomObject]
# ! D) Jedes Objekt ist kompatibel mit Typen die in der Vererbungshierarchie vorkommen:
(Get-Process)[0] -is [System.Diagnostics.Process]
(Get-Process)[0] -is [System.ComponentModel.Component]
(Get-Process)[0] -is [System.MarshalByRefObject]
(Get-Process)[0] -is [System.Object]

# ! 1. Eine Klasse kann von EINER anderen Basisklasse abgeleitet sein und erbt so dessen Member/Funktionen.
# ! 2. Jede Klasse ist früher oder später von der Klasse 'Object' abgeleitet worden.
# ! 3. Eine abgeleitete Klasse A ist daher mit der Basisklasse B kompatibel,
# ! daher kann auch ein Objekt von Typ A an einen Parameter vom Typ B übergeben werden.
# ? Welche Typ A von welchem Typ B abgeleitet wurde erfahren Sie so:
"Köln!".PSTypeNames
(Get-Process)[0].PSTypeNames
(Get-Process).PSTypeNames

#region Folgendes Beispiel demonstriert das Bindungsverfahren

function Test-PipelineBinding {
  [CmdletBinding()]
  param(
      [Parameter(ValueFromPipeline = $true)]                           [System.Object]$InputObject,
      [Parameter(ValueFromPipeline = $true)]                          [PSCustomObject]$InputPSCustomObject,
      [Parameter(ValueFromPipeline = $true)]         [System.ComponentModel.Component]$InputComponent,
      [Parameter(ValueFromPipeline = $true)]              [System.Diagnostics.Process]$InputProcess,
      [Parameter(ValueFromPipeline = $true)] [System.ServiceProcess.ServiceController]$InputService,
      [Parameter(ValueFromPipeline = $true)]                           [System.String]$InputString,

      [Parameter(ValueFromPipelineByPropertyName = $true)]
      [System.DateTime]$StartTime,

      [Parameter(ValueFromPipelineByPropertyName = $true)]
      [System.ServiceProcess.ServiceStartMode]$StartType,
      
      [Parameter(ValueFromPipelineByPropertyName = $true)]
      [System.String]$Name,
      
      [Parameter(ValueFromPipelineByPropertyName = $true)]
      [Alias('MachineName')]
      [System.String]$Rechnername
  )
  process {
      "1. Die Vererbungshierarchie des Pipeline-Objektes (Wichtig für ByValue-Bindung) sieht wie folgt aus:"
      $InputObject.PSTypeNames

      "`n2. Das Pipeline-Objekt besitzt u.a. folgende relevante Eigenschaften (Wichtig für ByPropertyName-Bindung):"
      Get-Process -Name notepad | 
          Get-Member -MemberType Properties | 
          ForEach-Object -Process {
              if ($_.Name -in 'StartTime', 'Name') {
                  $_.Definition
              }
      }
      
      #
      # $zerlegt = $_ -split " "
      # "`t[{0}]{1}" -f $zerlegt[0], $zerlegt[1]

      "`n3. Die daraus folgende Bindung (ByValue, ByPropertyName) gestaltet sich wie folgt:"
      
      [PSCustomObject]@{
          ParameterName        = 'InputObject'
          ParameterBindungsart = 'ValueFromPipeline'
          ParameterTyp         = [Object]
          ArgumentTyp          = $InputObject.GetType().FullName
          BindungErfolgreich   = 'JA'
      }
      
      [PSCustomObject]@{ 
          ParameterName        = 'InputPSCustomObject'
          ParameterBindungsart = 'ValueFromPipeline'
          ParameterTyp         = [PSCustomObject]
          ArgumentTyp          = $InputPSCustomObject.GetType().FullName
          BindungErfolgreich   = 'JA'
      }
      
      [PSCustomObject]@{ 
          ParameterName        = 'InputComponent'
          ParameterBindungsart = 'ValueFromPipeline'
          ParameterTyp         = [System.ComponentModel.Component]
          ArgumentTyp          = $InputComponent.GetType().FullName  
          BindungErfolgreich   = 'JA'
      }

      if($null -eq $InputProcess) {$argType = 'NULL'; $isBinding = 'NEIN'} else { $argType = $InputProcess.GetType().FullName; $isBinding = 'JA' }
      [PSCustomObject]@{ 
          ParameterName        = 'InputProcess'
          ParameterBindungsart = 'ValueFromPipeline'
          ParameterTyp         = [System.Diagnostics.Process]
          ArgumentTyp          = $argType  
          BindungErfolgreich   = $isBinding
      }
      
      if($null -eq $InputService) {$argType = 'NULL'; $isBinding = 'NEIN'} else { $argType = $InputService.GetType().FullName; $isBinding = 'JA' }
      [PSCustomObject]@{ 
          ParameterName        = 'InputService' 
          ParameterBindungsart = 'ValueFromPipeline'
          ParameterTyp         = [System.ServiceProcess.ServiceController] 
          ArgumentTyp          = $argType 
          BindungErfolgreich   = $isBinding
      }

      [PSCustomObject]@{ 
          ParameterName        = 'InputString'
          ParameterBindungsart = 'ValueFromPipeline'
          ParameterTyp         = [System.String]
          ArgumentTyp          = $InputString.GetType().FullName  
          BindungErfolgreich   = "JA (Value: '$InputString')"
      }
      
      if($null -eq $StartTime) {$argType = 'NULL'; $isBinding = "NEIN (Value: NULL)"} else { $argType = $StartTime.GetType().FullName; $isBinding = "JA (Value: '$StartTime')" }
      [PSCustomObject]@{ 
          ParameterName        = 'StartTime'
          ParameterBindungsart = 'ValueFromPipelineByPropertyName'
          ParameterTyp         = [System.Diagnostics.Process]
          ArgumentTyp          = $argType
          BindungErfolgreich   = $isBinding
      }

      if($null -eq $StartType) {$argType = 'NULL'; $isBinding = "NEIN (Value: NULL)"} else { $argType = $StartType.GetType().FullName; $isBinding = "JA (Value: '$StartType')" }
      [PSCustomObject]@{ 
          ParameterName        = 'StartType'
          ParameterBindungsart = 'ValueFromPipelineByPropertyName'
          ParameterTyp         = [System.ServiceProcess.ServiceStartMode]
          ArgumentTyp          = $argType
          BindungErfolgreich   = $isBinding
      }

      if($null -eq $Name) {$argType = 'NULL'; $isBinding = "NEIN (Value: NULL)"} else { $argType = $Name.GetType().FullName; $isBinding = "JA (Value: '$Name')" }
      [PSCustomObject]@{ 
          ParameterName        = 'Name'
          ParameterBindungsart = 'ValueFromPipelineByPropertyName'
          ParameterTyp         = [System.String]
          ArgumentTyp          = $argType
          BindungErfolgreich   = $isBinding
      }

      if($null -eq $Rechnername) {$argType = 'NULL'; $isBinding = "NEIN (Value: NULL)"} else { $argType = $Rechnername.GetType().FullName; $isBinding = "JA (Value: '$Rechnername')" }
      [PSCustomObject]@{ 
          ParameterName        = 'Rechnername (Alias=MachineName)'
          ParameterBindungsart = 'ValueFromPipelineByPropertyName'
          ParameterTyp         = [System.String]
          ArgumentTyp          = $argType
          BindungErfolgreich   = $isBinding
      }
  }
}

Get-Process -Name notepad # ! => notepad-Process
Get-Process -Name notepad | Get-Member # ! => vom Typ [System.Diagnostics.Process]
Get-Process -Name notepad  | Test-PipelineBinding | Format-Table
Get-Service -Name AudioSrv | Test-PipelineBinding | Format-Table

#endregion

# ? Aufklärung des Einführungsbeispiel
Get-Process -Name notepad | Get-Member      # 1. Analysieren des Rückgabe-Objektes bzgl. Type & Property
                                            # ! => System.Diagnostics.Process
Get-ChildItem -Path c:\temp\DL | Get-Member # 1. Analysieren des Rückgabe-Objektes bzgl. Type & Property
                                            # ! => System.IO.FileInfo
Get-Help -Name Stop-Process -ShowWindow     # 2. Welche Parameter lassen Pipelineeingabe zu und nach welchem Verfahren?
                                            # ! -InputObject <Process[]> ByValue
                                            # ! -Name <string[]> ByPropertyName
                                            # ! -Id <int32[]> ByPropertyName
                                            # 3. Was passt zw. 1. und 2. zusammen?

#region Beispiele für den praktischen Nutzen

#region z.Bsp.: Alle Fehler in einer MasterError.log sammeln
Get-ChildItem -Path "$env:WinDir\Logs" -Recurse -File -Force | # IN: - OUT: FileInfo
    Where-Object -Property Extension -EQ  -Value ".log" |      # ByValue IN: FileInfo OUT: FileInfo
    Select-String -Pattern "error" |                           # ByValue IN: FileInfo OUT: MatchInfo
    Set-Content c:\temp\master_error.log                       # ByValue IN: MatchInfo OUT: -
#endregion

#region z.Bsp.: Über eine CSV-Datei neue Benutzer anlegen
@"
Benutzername;Passwort;Beschreibung
p.lustig;P@ssw0rd;Peter Lustig (IT)
e.gruen;Geh1imAbc;Eva Grün (HR)
"@
 | Set-Content -Path c:\temp\NewUsers.csv

Get-Content -Path C:\temp\NewUsers.csv |
  ConvertFrom-Csv -Delimiter ";" -Header Name, Password, Description | 
  Select-Object -Skip 1 |
  ForEach-Object -Process { 
        $_.Password = $_.Password | ConvertTo-SecureString -AsPlainText -Force # ! ACHTUNG geht nur wenn die Property 'set' erlaubt
        return $_
    } | 
  New-LocalUser

  # vs.

Get-Content -Path C:\temp\NewUsers.csv |
  ConvertFrom-Csv -Delimiter ";" -Header Name, Password, Description | 
  Select-Object -Skip 1 |
  ForEach-Object -Process { 
        $pwd = $_.Password | ConvertTo-SecureString -AsPlainText -Force
        $_ | Add-Member -Name Password -Value $pwd -MemberType NoteProperty -Force -PassThru
    } | 
  New-LocalUser -WhatIf

  # vs.

Get-Content -Path C:\temp\NewUsers.csv |
  ConvertFrom-Csv -Delimiter ";" -Header Name, Password, Description | 
  Select-Object -Skip 1 -Property Name, `
                                  Description, `
                                  @{Label="Password"; Expression={$_.Password | ConvertTo-SecureString -AsPlainText -Force}} |
    New-LocalUser -WhatIf

#endregion

#endregion

#endregion

#region Splatting (Mehrere Parameter gleichzeitig an ein Cmdlet übergeben)

Get-EventLog -LogName System -Newest 5 -EntryType Error, Warning
# vs.
$argument = @{
    LogName   = 'System'
    Newest    = 5
    EntryType = 'Error', 'Warning'
}
Get-EventLog @argument

#endregion

#region Übungen

<# TODO Übung 1 (Pipelining)
        ? A) Die Sortierung bei der folgenden Zeile funktioniert nicht, warum?
             Get-ChildItem $env:WinDir\*.exe | Select-Object -Property Name, LastWriteTime | Sort-Object -Property Length
        ? B) Welche Variante ist die Ökologischste, warum und wie haben Sie das ermittelt?
             Get-EventLog -LogName System | Where-Object -Property EntryType -EQ 'Warning'
             Get-EventLog -LogName System | Where-Object -FilterScript { $_.EntryType -eq 'Warning' }
             Get-EventLog -LogName System -EntryType Warning
        ? C) Warum wird das Gast-Konto nicht gefunden?
             Get-CimInstance -ClassName Win32_UserAccount | Where-Object { $_.Name = 'Gast' }
        ? D) Warum funktioniert der folgende Befehl nicht?
             Get-Process | Format-Table -Property Name, Company | Sort-Object -Property Company
        ? E) Warum werden keine doppelten Objekt entfernt?
             1,2,3,1,2,3,1,2,3 | Get-Unique
        ! TIPPS: -
        * MUSTERLÖSUNG
#>


<# TODO Übung 2 (Pipelining)
    ? A) Anhand einer ToDeletedFiles.csv-Datei sollen nur die darin enthaltenen Dateinamen im Temp-Ordner gelöscht werden.
    ! TIPPS: Get-Content; ConvertFrom-Csv; Remove-Item; Get-ChildItem; ForEach-Object; Add-Member
    * MUSTERLÖSUNG 76492d1116743f0423413b16050a5345MgB8AHAALwB1ADkAbQBvAGEAMwBYAHEAZAA0AG4AVgBsAE0AYgAvAEIANwBlAFEAPQA9AHwANQA2AGQANQA1ADEAYQA2ADEAMABlADMAMgA5ADMAYgBmADAAZAA0AGQAOAA4ADYAYwBlADcAOABmADcANAAzAGQAMwBjAGMAOAA1AGQAZQBhADAAYQBlADgAZQAyADIAYwBhADkAMwBlAGIAZAAzADYAOAAyAGEAZgA0ADQAMABjADUAZAAzADQAMwBjADIAYwAxADYAZABkAGIAYwBkAGEAYgA0AGQAMwBiAGQAMABlAGUAMAAzAGIANwA0AGIANQAzADMAMwBkADYAZQAwAGIANQBmADIAMwA3AGEANQBjADcAYQA2ADQAZgA1ADUAYQBjAGUANQBmAGIAOABhAGIAYwBkADIAYQBjADQAOQA1ADYANwBiAGIAMAAwAGYAMQAwADMAMQAwADAANQBhAGYAMAA1ADgAMwAzADMAZQAwADIAYwBlAGQAYwBkADUAOQA0ADgAZAA3ADcAYQBmAGEANAA4ADEAMQBmAGQAZAA1ADUAZQBhADQANAA3ADcAZgA4AGUANwBkADAAZQAyADkAMQBkADkAMgA4AGUAYQA5ADAAYwA2ADgAYQAyAGEANQAwADYAMgBlAGQAMQA1ADkAYQAwAGEAYgAwAGQAMQA0AGMAOQAwADkAOAA4AGYAMABlAGIAYgBjAGIAZQA4AGQAOABjADYAYgBkAGQAZQBhAGIAMAA0ADcAZAAxADYAYwBmADIAMQBiADQAYQAyADQAZgBkADAAZgBlAGEANQA2AGQAMQA1ADEAZgA1AGYANQBjAGMANwBjAGQANwAzADIANwA3AGUAZQBkAGIAOQBiADYAOABmADkANAA4AGQANAAwADAANAA5ADkAZgBlADQAZAAxAGMAZgA4ADYAOQBmAGIAYQAzADEAMAA4AGUANwA2AGMAOABmADQAYgA3ADgAYgAxADMAYgBmAGEAZAAwAGQAMgA2ADYAOABlADYAYgA5ADIAOQAyADgAZAA3AGUAZAAxADEAOQAxADkAYgAxAGUAZgBjAGUAYQBkADEANgBmADMAMgBhADQAOAAyADAANQBiAGQAMQBlAGIAZABmAGIAYgBlADcAYwAzADQAMQBiADgAOQA1ADEAYwA0ADkANwBiADQAZQBlADYAMAAyADYANwA0AGYAOQBkADgANwAzADgANgA0AGEAMQA2AGYAMQAyAGUAMQAwADEAZAA5AGMAYQBlADEAOAAyADEAYgAwADQAZABhAGEAYgBiAGUAMwA4ADAANwAxADcAOAA2ADgAZQA4ADIAZQBiADIAMABjADAAMwA5AGUANAA5AGIAYQA4ADgAYgBiADUANgAwADgANQA0ADgANwA4AGMAZQA0ADcAYgA0ADgAMwAwAGEAYwAxADMANwBhADYAOAA0ADcAZgBkAGYANwA4AGYANABjADUAMgA2ADMANwA5ADcAOAA5ADUAYwA1ADYAZQA2ADYAZQAyADQAZgAxADIAYQBhAGEAYwA4ADMAYQBkADkAOQBlADYAMQBkADgAYQA5ADYAYgAwADYAYgAzADMAYgAyAGYAMgBiAGMANgA1ADAANQAwADAAMAAyADgAYQBlADkANQAwADQANgA3ADYAMQA0AGUAYwAwADQAYQBjADYAZABhADMAYgBkADAANgA4ADUAZABkADkAYwA0AGEANQAwAGMAYQBiADMANQA3ADAAZAA3ADgANgA0AGUAYQA1AGUAYQA0ADYAMAAzADYAZAAwADAAZQA3ADYAOAA0ADUAYgAxAGMAZAAwADUAMwBmADAAMgA4ADAAMgA1AGIAMQAyADEAZgA0AGYAZABjADUAZgA0ADgAYgA3AGUAYwA1ADgAOQAxADYAYgBhADMANwA2AGMANgA1ADkAYQA0ADYAMQBmAGQAYgBkAGYAMQBmADYAOQAwAGIAMgAwAGYAMABiADkAYgA5ADMAOQAzAGYANgA0ADUAYwBiAGUAYQBhADAANAA3ADAAOQA1ADIAOQAzAGMAZQA5ADIAYwAxADkAZgA5AGMAMQA1ADMAYwBlADEANQAyAGMAMABmADkAMgBlADIAYwBkADIANQBmADUAMABiAGIAZABkAGUANwAyAGEAMgAzADEAZgA4ADIAMQA5AGYAMgBlADQAZQBjADkANAA2AGQAMwBmADUAOAAwADgANQA3AGIAYgAyAGUAMwBlAGMAOAA0ADIAOQBhADkAZgBmADkAOAA5ADEAOAA1AGQAMgAwADkAZgA0AGIAOAA4AGQANABkADQAMwBlADUAYwA4ADIAOQAzADkAYQAxADgAMQAzAGMAZAA4ADYAMQAxADEAMQBjADgAOQAzAGQAOABhAGEAYwA0ADkAYwBjAGMAYwAxADIAZgA2ADcANAA4AGIAMgA1ADQAOABjAGUAYgBlADYAYgBkAGUAZAA3ADgAOABhAGIAMQA2AGIAMgAzADEAMwA3ADgAMgAyADkAMAAwAGIANABhAGYAYwBhAGUAYQBmAGUAZQBkADEAZQA5ADIAMgAwAGIANwBiAGYAZQAzADQAZgBmADcAZQBkADQANAAwADQAZgA1ADUAMwBlAGEANQA3AGEAYwBkADcAMQAxADMANwAzADcAMQAwADkAMgAzADYANwAwAGQAMQBkAGQAZgBiADcAMAAxAGEAMQAxAGYANQA3AGUAZABkADQAZAAyADMAMQBhADYANwA3AGMAOABlAGMAMwAwADQANgBjADAANQAyADkAYgA2AGIANwA3AGYAMgA1ADYAYwA5ADUAMAA4ADMANABlADAAOABiAGUAMAA2ADgAZQBiAGIAZgBiAGQAZAA5ADgAYgBiADMAMQA5ADMAYwA4ADQAYgBkADEAOQAxADcAZgA4ADIAOQBiADAAMwBlADkAYQBjADgANwAwADcAMgAyADcAZQA5ADEAYgBlAGMAZABiADYAMgA0ADYAYgA1AGYAYQA1ADAAMAAzAGYAMwBkADcAZgAyADMAMgA5ADAAYQA3ADcAMQBkAGIAOQAyADgAOQA2ADcAYQA3ADEAOAAyADkANgAyAGYANABiADcAOQBmADIAMQBhAGYAMwA1ADcAOABlADIAZAA3ADUANgBmADIAMAAzADAAOQBhAGIANABjADYAMABjAGIAYQAxADEAOAA5ADQAZgA4ADUAMwBkADQAMQAwAGEAZABkAGYAYQBlADIAYgBiADgAMwA0ADQANAA3ADIANQA5ADcANABhADIAMQBkADUANgAyADcANABiAGIAZABmADkAMQA5ADkAYQAzADUAMABhAGMAOAA4AGEAYgAxADEAMwBlAGMAMAA0ADUAOABiAGEAMwA1ADMANAAxAGEAYQA2ADgANQBkAGUAYQA5AGYAOABhAGMANQBlADMAMgA2AGQAZQA1AGIANgA4AGMAMQBlAGEAMwBhADUAMAA2AGIANQA4AGUAZgAzADQAZQBmADQANABmAGMAOQBmADEANgA0ADkAMQBhAGQAYQAyAGMAMwA2AGEAOQAzADUAMgAwAGUAMwA2ADMAMwA0ADIAOQBjADIAMgBhAGIAZAA2ADcAOAA4ADAAYwA3AGYANQAwAGQANwAzADIAOQAxADcANwBhADAAMgBkADYANQBhADIAYwA4ADcAZgAwADEANgBmADEAZgA5AGMAYQBkAGYAYgAxAGUAYQBhAA==
#>


#endregion