Как найти версию, которую я установил

In this series, we have thoroughly discussed .NET namespaces and classes. Now, let’s shift our focus to properties and methods.

Advanced PowerShell Techniques with the .NET Framework

  • Using .NET Properties and Methods

Before diving in, here’s the sample PowerShell script we have been using:

ray-so-export.png

How do I find the version of .NET that I have installed?

Updated On:

Products

Client Management Suite

Issue/Introduction

How do I find the version of .NET that I have installed?

Environment

Windows Server 2016

Resolution

There are a few ways to determine the version of .NET that is installed.

This is the output that you will see after running the Powershell command above:

Как найти версию, которую я установил

This is the output that you will see after running the Powershell command above:

Как найти версию, которую я установил

3. Open the Registry Editor and go to: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP

Как найти версию, которую я установил

Please note in all of these examples the latest version of the .NET Framework installed is 4.8.

Feedback

Calling a Method Directly

We’ve covered basic techniques for working with classes, properties, and methods. Let’s introduce one more concept. In previous examples, I used the cmdlet to create an object linked to a namespace and a class, such as:

$Form = New-Object System.Windows.Forms.Form

Although this is the normal way of doing things, some classes lack a required constructor, so you can’t use them with the New-Object cmdlet. For instance, to check if a file named C:\test.txt exists on my hard drive, I might try:

$File = New-Object System.IO.File

is the namespace, is a class, and is a method within the File class. The Exists method requires a filename (and optional path) in string format and returns if the file exists or if it doesn’t.

However, the File class lacks the required constructor, resulting in an error (see Figure 2).

PowerShell screenshot demonstrates error when attempting to create an object using File class

You cannot create an object using the File class.

To work around this, call the method directly. We could use this command in this case:

Here, the namespace and the class name are enclosed in brackets. The two colons indicate to PowerShell that a .NET method is being called. Append the method name just after the two colons and supply any required parameters (in this case, a path and filename). See what the command looks like in Figure 3.

PowerShell screenshot shows [System.IO.File]::Exists(“C:\Temp\Test.txt”) command

You can access some .NET methods directly.

  • Microsoft.Management.Infrastructure.CimCmdlets 7.3.8
  • System.Management.Automation 7.3.8
  • Microsoft.Management.Infrastructure 2.0

When executing this line:
var rs = System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace();
or this line:
var ps = System.Management.Automation.PowerShell.Create();

I get this error:

Exception thrown: 'System.IO.FileNotFoundException' in System.Private.CoreLib.dll
An exception of type 'System.IO.FileNotFoundException' occurred in System.Private.CoreLib.dll but was not handled in user code
Could not load file or assembly 'Microsoft.Management.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. The system cannot find the file specified.

EDIT: Stack trace

at System.Reflection.RuntimeAssembly.GetExportedTypes() at System.Management.Automation.Runspaces.PSSnapInHelpers.GetAssemblyTypes(Assembly assembly, String name) at System.Management.Automation.Runspaces.PSSnapInHelpers.AnalyzeModuleAssemblyWithReflection(Assembly assembly, String name, PSSnapInInfo psSnapInInfo, PSModuleInfo moduleInfo, String helpFile, Dictionary`2& cmdlets, Dictionary`2& aliases, Dictionary`2& providers) at System.Management.Automation.Runspaces.PSSnapInHelpers.AnalyzePSSnapInAssembly(Assembly assembly, String name, PSSnapInInfo psSnapInInfo, PSModuleInfo moduleInfo, Dictionary`2& cmdlets, Dictionary`2& aliases, Dictionary`2& providers, String& helpFile) at System.Management.Automation.Runspaces.InitialSessionState.ImportPSSnapIn(PSSnapInInfo psSnapInInfo, PSSnapInException& warning) at System.Management.Automation.Runspaces.InitialSessionState.CreateDefault() at System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace(PSHost host) at System.Management.Automation.Runspaces.RunspaceFactory.CreateRunspace()

Not sure why the nuget package depends on 2.0 version of the DLL but still wants to load 1.0. I also get this warning in VS wn building the project:

1>C:\Program Files\dotnet\sdk\8.0.100-rc.2.23502.2\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.targets(284,5): warning NETSDK1206: Found version-specific or distribution-specific runtime identifier(s): win10-x64, win10-x86, win7-x64, win7-x86, win81-x64, win81-x86, win8-x64, win8-x86. Affected libraries: Microsoft.Management.Infrastructure.Runtime.Win. In .NET 8.0 and higher, assets for version-specific and distribution-specific runtime identifiers will not be found by default. See https://aka.ms/dotnet/rid-usage for details.

Does it mean it is not possible to use PowerShell SDK 7.x nuget package with .net8 because of the above warning and version-sepcific DLLs not being loaded by default? I tried also loading it manually, with no success (System.Reflection.Assembly.LoadFrom(pathToDll)).

Would it work if I downgraded to .net7 for instance?
If all fails – is the only workaround to run powershell.exe using Process class?

EDIT 2: I can reproduce this on 2 development machines (win10 and win11)

Время на прочтение

Все привет! В продолжение статьи о возможностях PowerShell, хочу поделиться несложной реализацией создания REST API и простого Web-сервера, используя только PowerShell на базе класса .NET HttpListener. Такая реализация позволяет настроить endpoint-ы (конечные точки) для обработки GET и POST запросов, которые принимают параметры в заголовке запроса, использовать Basic-авторизацию (на основе Base64), обрабатывать любые коды возврата, а так же без лишнего кода, отдавать информацию в разных форматах: json, xml, html, csv. Хочу акцентировать, что данное решение, возможно, не является самым правильным, тем не менее успешно помогло мне покрыть потребности нескольких задач и хотел лишний раз продемонстрировать возможности языка.

Для начала расскажу, кому и зачем данная реализация может понадобиться, далее приведу пример работы с готовым решение и разберем основные моменты для создания своего сервера. Стоит сразу упомянуть, что существует кросс-платформенный Web Framework Pode, и это не единственное решение, успел попробовать как минимум три, но, пожалуй, самое интересное, поддерживаемое и задокументированное. Мне же хотелось иметь свою реализацию, где будут отсутствовать сторонние зависимости, и немного больше понимать, что происходит на стороне сервера во время его работы, в частности, для отладки.

:/>  Стандартные игры в Windows - где их найти?

Кому и зачем данная реализация может понадобиться? Приведу свой пример, у меня была задача удаленно реализовать доступ к десктопному приложению, у которого для специфического взаимодействия с ним была возможность выполнять только локальные команды через консоль. Из условий, не было возможности настроить и использовать на машинах WinRM и OpenSSH, ввиду ограничений в компании со стороны ИБ, в то же самое время HTTP был валидным и стандартизированным (после HTTPS) решением. В результате, выполнение и вывод команд получилось автоматизировать, а в дополнение к этому, добавить настройки реестра, чистку temp и логов, что расширило возможности и позволило инженеру DevOps внедрить их в свой Pipeline, используя привычный интерфейс.

Во-вторых, работая системным администратором, в качестве интерфейса для автоматизации задач я использовал WinForms, редко Telegram. Тогда мне очень хотелось попробовать реализовать свой Web-интерфейс для визуализации подобных задач. Важно заметить, что не обладаю сильными познаниями в области систем CI/CD, и конечно, рациональнее использовать, например, интерфейс Jenkins для подобных целей. Тем не менее подобное решение имеет место, т.к. Jenkins все таки не содержит такой кастомизации, как собственный интерфейс.

В-третьих. У меня есть небольшой проект, целью которого является поиск и доставка контента из конкретного torrent-трекера Кинозал до телевизора с Plex (на эту тему у меня есть отдельная статья на Habr). Так сложилось, что за основу я выбрать язык Bash, т.к. планировал запускать бота удаленно и использовать только REST API интерфейс для взаимодействия со всеми сервисами. В течении всего времени эксплуатации, мне не хватало такого функционала, как остановка и повторный запустить torrent-клиента (qBittorrent), или просматривать свободное место на диске, узнать размер конкретных директорий и файлов, а так же возможности их удаления. По аналогии с другими сервисами, мне хотелось использовать единый интерфейс (REST API), в попытках найти готовое решение в виде десктопного приложения для Windows, вспоминая, что уже взаимодействовал с Open Hardware Monitor, используя его как клиент (в режиме HTTP), уже писал модуль для получения метрик через REST API (с возможностью отправки их в InfluxDB и визуализацией в Grafana). Но этого было мало, например для просмотра и удаления файлов можно настроить сервер Everything (который тоже имеет HTTP-сервер). Тут я понял, для покрытия нескольких специфических и не сложных задачи устанавливать дополнительно 2-3 сервиса нерационально, по этому решил написать отдельное решение.

Далее, речь пойдет про WinAPI, решение, с помощью которого у меня получилось покрыть все мои потребности, а конкретно: удаленная остановка и запуск служб и процессов, вывод метрик, максимально приближенных к диспетчеру задач (список физических и логических дисков, показатели IOps, потребление RAM, нагрузка CPU, количество запущенных процессов, потоков, дескрипторов и т.п.), а так же просматривать список директорий и файлов, их размер и количество, с возможностью удаления.

Как это выглядит на практике. Для установки и запуска данного сервиса я попробовал реализовать два решения. Запуск в виде исполняемого файла (используя модуль ps2exe), в таком варианте можно запускается отдельная консоль, где можно наблюдать весь лог и завершать процесс при закрытии консоли. Второй вариант, это запуск фонового процесса, в таком случае для чтения лога используется файл. Но такое решение не самое удачное, т.к. модуль имеет ограничение, которое позволяет запускать любой скрипт только в PowerShell 5.1 и командлеты из версии Core попросту не буду работать.

Второй вариант, это запуск в качестве службы, процесс установки получилось так же автоматизировать, достаточно на любой машине, где есть доступ в интернет запустить этот скрипт. Настроить свои данные для авторизации и задать свой номер порта (предварительно открыть его в firewall) в конфигурационном файле, после чего, начать взаимодействовать на любой системе, используя Invoke-RestMethod или Curl:

Пример удаленной остановки используя PowerShell и запуска службы из Linux (последним запросом я проверил статус службы самого WinAPI).
Пример удаленной остановки используя PowerShell и запуска службы из Linux (последним запросом я проверил статус службы самого WinAPI).
lifailon@hv-devops-01:~$ user="rest"
lifailon@hv-devops-01:~$ pass="api"
lifailon@hv-devops-01:~$ curl -s -X GET -u $user:$pass http://192.168.3.100:8443/api/service/winrm # запрашиваем статус службы WinRM
{ "Name": "WinRM", "DisplayName": "Служба удаленного управления Windows (WS-Management)", "Status": "Stopped", "StartType": "Automatic"
}
lifailon@hv-devops-01:~$ curl -s -X POST -u $user:$pass --data '' http://192.168.3.100:8443/api/service/winrm -H "Status: Start" # запускаем службу
{ "Name": "winrm", "DisplayName": "Служба удаленного управления Windows (WS-Management)", "Status": "Running", "StartType": "Automatic"
}

Пример простого Web-сервера был скорее эксперимент, чем необходимость (очень уж хотелось закрыть старый гештальт). Тем не менее выглядит решение так:

Список служб с отображением в формате HTML и возможностью их остановки и запуска.
Список служб с отображением в формате HTML и возможностью их остановки и запуска.

Естественно, для обработки кнопок в браузере не обошлось без JavaScript, особых познаний языка тут не требуется, нашел буквально первый пример в интернете как создать кнопки и обработать их действие при нажатии, ознакомившись с основами HTML-синтаксиса и додумав логику, все получилось сделать достаточно просто. Вот пример с комментариями:

# Типовое условие для проверки вхождения на соответствие метода (GET) и конечной точки (/service)
elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/service") { # Получаем массив из списока служб, используя кастомную функцию для вывода с подробной информацией $Services = Get-ServiceDescription * # Формируем текст HTML-документа, задаем заголовок страницы и открываем тело страницы $GetService = "<html><head><title>Service</title></head><body>" # Добавляем заготовленные кнопки, которые перенаправляет на другие url $GetService += $BodyButtons # Указываем на создание таблицы и задаем имена столбцов $GetService += "<table border='1'>" $GetService += "<tr><th>Name</th><th>Status</th><th>Action</th><th>Start Type</th></tr>" # Передаем в цикл список служб и забираем значения foreach ($Service in $Services) { $name = "<b>$($Service.Name)</b>" $status = $Service.Status # Проверяем статус службы, если работает, красим в зеленый цвет if ($status -eq "Running") { $status = "<font color='green'><b>$status</b></font>" } else { $status = "<font color='red'><b>$status</b></font>" } $StartType = $Service.StartType # Заполняем значения столбцов, по анологии с наименованием столбцов (в блоке <tr>) $GetService += "<tr><td>$name</td><td>$status</td>" # Создаем кпноки, которые при нажатии ссылаются на функции startService и stopService, которые в качестве параметра передают наименование службы $GetService += "<td><button onclick='startService(""$($Service.Name)"")'>Start</button> " $GetService += "<button onclick='stopService(""$($Service.Name)"")'>Stop</button></td>" $GetService += "<td>$StartType</td></tr>" } $GetService += "</table>" $GetService += ' # Формируем в блоке <script> функции, для обработки нажатия на кнопки <script> function startService(serviceName) { sendServiceAction("Start", serviceName); } function stopService(serviceName) { sendServiceAction("Stop", serviceName); } # Данная функция принимает действие и отправляет соответствующий POST-запрос, для его обработки другой конечной точкой function sendServiceAction(action, serviceName) { var request = new XMLHttpRequest(); request.open("POST", "/api/service/" + serviceName, true); # В заголовок запроса передаем статус с содержимым действия (Status: <Stop/Start>) и обновляем страницу (reload) request.setRequestHeader("Status", action); request.onreadystatechange = function () { if (request.readyState === 4 && request.status === 200) { console.log("True"); location.reload(); } }; request.send(); } </script> </body></html> ' # Передаем сформированные данные и код ответа в функцию, для отправки ответва клиенту Send-Response -Data $GetService -Code 200 -v2
}

Для сравнения интерфейса, приведу пример управления службами, используя простой Jenkins Pipeline. Из явных преимуществ, такой интерфейс универсален для обеих систем (Windows и Linux), логика преимущественно на PowerShell и Bash (в моем случае), а доступ настраивается централизованно через Ansible, где в свою очередь используя ssh и winrm. Такой доступ можно заменить на REST-запросы, при наличии подобного сервера на каждой удаленной машине (например, в виде установленной службы). Безусловно, это более современное и правильное решение, но не взаимозаменяемое, речь только про интерфейс взаимодействия, где мы можем в одном интерфейсе управлять сразу с несколькими машинами.

:/>  Как проверить работу винчестера на пк
Jenkins Pipeline для запуска и остановки служб
Jenkins Pipeline для запуска и остановки служб

По аналогии со службами, обработал остановку и запуск процессов.

Список процессов с возможностью их завершения и запуска по имени.
Список процессов с возможностью их завершения и запуска по имени.

Из интересного на мой взгляд, написал простую функцию для поиска исполняемого файла в системе, который отвечает за запуск процесса конкретного приложения. Если такой процесс не получается найти, то мы получим в ответ код 400: Bad Request. Process <$ProcessName> could not be found. В таком случае, можно воспользоваться заголовком Path, который принимает путь до исполняемого файла.

function Find-Process { param ( $ProcessName ) $ProcessPath = (Get-ChildItem "C:\Program Files" | Where-Object Name -match $ProcessName).FullName if ($null -eq $ProcessPath) { $ProcessPath = (Get-ChildItem "C:\Program Files (x86)" | Where-Object Name -match $ProcessName).FullName } if ($null -eq $ProcessPath) { $ProcessPath = (Get-ChildItem "$home\AppData\Roaming" | Where-Object Name -match $ProcessName).FullName } $ProcessNameExec = "$ProcessName"+".exe" (Get-ChildItem $ProcessPath -Recurse | Where-Object Name -eq $ProcessNameExec).FullName
}
> Find-Process qbittorrent
C:\Program Files\qBittorrent\qbittorrent.exe
> Find-Process nmap
C:\Program Files (x86)\Nmap\nmap.exe
Find-Process telegram
C:\Users\lifailon\AppData\Roaming\Telegram Desktop\Telegram.exe

Для сбора метрик используется CIM (Common Information Model). Сам скрипт сервера, описание с примерами, как и набор функций опубликованы на GitHub.

Получение информации о системе через CIM.
Получение информации о системе через CIM.

Так как PowerShell Core является кросс-платформенным решением, class System.Net.HttpListener работает и в системе Linux, используя такую же логику и возможности сразу нескольких языков (например, Bash), можно управлять службами на платформе Windows через systemctl используя REST API.

Что важно, при возникновении ошибки, мне хотелось, что бы данный сервер только логировал ее, но при этом продолжал функционировать (фактически, перезапускался). Для это достаточно вынести слушателя с циклом в отдельную функцию и запускать ее внутри еще одного бесконечного цикла, где присутствует дополнительная обработка ошибок в блоках try-catch-finally.

Вот базовый пример, без лишнего кода с описанием:

# Заполняем переменны с номером порта и данными для авторизации
$port = 8443
$user = "rest"
$pass = "api"
# Формируем строку Base64 из данных логина и пароля
$cred = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("${user}:${pass}"))
# Функция для логирования запросов
function Get-Log { ### Debug (Get all Request, Headers and Response parameters):	# Используя содержимое запросов (Request), чтение передаваемых заголовков и ответа (Response), можно расширить возможности логирования для отладки процесса # $context.Request | Out-Default # foreach ($header in $context.Request.Headers) { # Write-Host "$header = $($context.Request.Headers[$header])" # } # $context.Response | Out-Default	# Забираем содержимое из запроса: адрес клиента, наименование агента, метод и url конечной точки $remote_host = $context.Request.RemoteEndPoint $client_agent = $context.Request.UserAgent $method = $context.Request.HttpMethod $endpoint = $context.Request.RawUrl $response_code = $context.Response.StatusCode $date = Get-Date -Format "dd.MM.yyyy hh:mm:ss"	# Выводим в консоль или в файл "$date $remote_host $client_agent => $method $endpoint => $response_code" # "$date $remote_host $client_agent => $method $endpoint => $response_code" | Out-File $Log_Path -Encoding utf8 -Append
}
# Функция для ответа клиенту
function Send-Response { param ( $Data, [int]$Code ) # Проверяем код ответа, если он равен 200 (успех), то конвертируем данные перед отправкой клиенту if ($Code -eq 200) { # Дополнительно можем проверить название агента на клиентской стороне, который может выступать в роли браузера или явно задан тип данных HTML if (($context.Request.UserAgent -match "Chrome") -or ($context.Request.ContentType -match "html")) { # Конвертируем полученные данные в HTML и указываем тип контента в ответе	$Data = $Data | ConvertTo-Html $context.Response.ContentType = "text/html; charset=utf-8" }	# Далее проверяем только тип контента из заголовка (если он задан явным образом), и конвертируем вывод в соответствующий тип данных elseif ($context.Request.ContentType -match "xml") { $Data = ($Data | ConvertTo-Xml).OuterXml $context.Response.ContentType = "text/xml; charset=utf-8" } elseif ($context.Request.ContentType -match "csv") { $Data = $Data | ConvertTo-Csv $context.Response.ContentType = "text/csv; charset=utf-8" }	# По умолчанию, конвертируем в JSON else { $Data = $Data | ConvertTo-Json $context.Response.ContentType = "text/json; charset=utf-8" } } # Указываем код статуса для ответа $context.Response.StatusCode = $Code # Преобразуем данные в массив байтов, используя кодировку UTF-8 (особенно важно, при передачи в формате HTML) $buffer = [System.Text.Encoding]::UTF8.GetBytes($Data) # Выполняем функцию логирования Get-Log # Забираем число количества байт буффера для записи в поток, который передается в параметр ответа. Это является важным условием, что все даныне были переданы и прочитаны на стороне клиента. $context.Response.ContentLength64 = $buffer.Length # Передаем массив байтов (наш буффер ответа с данными) в поток ответа, обязательно нужно передать параметры смешения (если бы нужно было начать запись с определенного места в массиве) и длинны буффера $context.Response.OutputStream.Write($buffer, 0, $buffer.Length)	# Данный метод обновляет буфер вывода, убеждаясь, что все данные из буфера отправлены клиенту $context.Response.OutputStream.Flush()	# Закрываем поток ответа $context.Response.OutputStream.Close()
}
# Создаем сокет слушателя
Add-Type -AssemblyName System.Net.Http
$http = New-Object System.Net.HttpListener
# Указываем адрес слушателя (+ что бы слушать на всех интерфейсах) и порт
$http.Prefixes.Add("http://+:$port/")
# Указываем использование базового метода аутентификации
$http.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Basic
# Запускаем сокет (начинаем слушать запросы на указанном порту)
$http.Start()
# Обработчик try-finally нужен для закрытия сокета в случае его непредвиденного завершения
try { # Отправляем в бесконечный цикл прослушивание входящих запросов, пока свойство IsListening объекта $http равно true while ($http.IsListening) { # Используем асинхронный режим, для ожидания новых запросов $contextTask = $http.GetContextAsync() # Синхронно ожидает завершения асинхронной задачи, чтобы дождаться завершения асинхронной операции, прежде чем продолжить выполнение кода while (-not $contextTask.AsyncWaitHandle.WaitOne(200)) { }	# Получение результата асинхронной задачи $context = $contextTask.GetAwaiter().GetResult() # Проверяем полученные данные авторизации (в формате Base64) из заголовка запроса на соответветствие переменной $cred $CredRequest = $context.Request.Headers["Authorization"] # Write-Host $CredRequest $CredRequest = $CredRequest -replace "Basic\s" if ( $CredRequest -ne $cred ) { # Если авторизационные данные не прошли проверку (неверно передан логин или пароль), передаем в функцию ответа параметры с текстом ошибки и кодом возравата 401 $Data = "Unauthorized (login or password is invalid)" Send-Response -Data $Data -Code 401 } else { # Если авторизация прошла, проверяем метод и url конечной точки на соответветствие, что бы его обработать if ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/service") { $GetService = Get-Service -ErrorAction Ignore Send-Response -Data $GetService -Code 200 } # Дальше по аналогии дополнительными условиями (elseif) добавляем обработку других конечных точек elseif ($context.Request.HttpMethod -eq "GET" -and $context.Request.RawUrl -eq "/api/process") { $GetService = Get-Process Send-Response -Data $GetService -Code 200 } # Если не одно из методов не прошел соответветствие, отправляем ответ с кодом 405 elseif ($context.Request.HttpMethod -ne "GET") { $Data = "Method not allowed" Send-Response -Data $Data -Code 405 } # Если не одно из условий не подошло, отправляем ответ с кодом 404 else { $Data = "Not found endpoint" Send-Response -Data $Data -Code 404 } } }
}
finally { # Освобождаем сокет $http.Stop()
}
Пример работы базового REST API сервера с обработкой ошибок.
Пример работы базового REST API сервера с обработкой ошибок.

Итог. Взяв за основу такой скелет и доработав под себя логику, можно автоматизировать процессы конкретного приложения, у которого не предусмотрен внешний интерфейс для подобных задач. В свою очередь, на стороне клиента взаимодействовать с ним, используя любой удобный интерфейс с REST-клиентом.

:/>  11 способов запустить блокнот в windows (все версии) - Производительность - 2021

Exploring .NET Properties

The sample script has several properties, such as . Both Width and Height are properties of the Form class.

To explore properties and methods, visit the .NET API browser. Click on a namespace to see its classes, then select a class to view its associated elements, such as properties, methods, fields, and events.

Properties are essentially attributes that can be applied to an object. Our sample script sets a button’s text and background color using properties.

Navigating Syntax and Examples

Believe it or not, figuring out the right classes, properties, and methods is the hardest part. The actual syntax is fairly intuitive.

When you click on a property or a method in the .NET API browser, it opens a page providing syntax and examples, typically written in C#. Even though the examples are in C#, they are useful for understanding the values needed for properties or methods. For instance, in Figure 1, requires an integer value representing the form’s width in pixels.

screenshot of the .NET API browser interface

Help is available for all the properties and methods.

About the Author(s)

Brien Posey

Brien Posey is a bestselling technology author, a speaker, and a 20X Microsoft MVP. In addition to his ongoing work in IT, Posey has spent the last several years training as a commercial astronaut candidate in preparation to fly on a mission to study polar mesospheric clouds from space.

Properties That Reference Methods

Occasionally, you may find that properties reference methods. Consider this line of code from the script: