Январь 2016

Том 31, номер 1

Главное в .NET - Написание скриптов на C#

Марк Михейлис | Январь 2016

Марк МихейлисС выходом Visual Studio 2015 Update 1 (далее для краткости Update 1) появился новый C# REPL (read-evaluate-print-loop), доступный как новое интерактивное окно в Visual Studio 2015 или как новый интерфейс командной строки (command-line interface, CLI), названный CSI. В дополнение к существующему языку C# в командной строке Update 1 также вводит новый скриптовый язык C#, традиционно сохраняемый в CSX-файл.

Прежде чем погружаться в детали новой поддержки написания скриптов на C#, важно понимать целевые сценарии. Поддержка скриптов на C# — это средство для тестирования ваших фрагментов кода на C# и .NET, не прикладывая усилия к созданию множества проектов модульного тестирования или консольных проектов. Она предоставляет облегченный вариант для быстрого кодирования вызова LINQ-метода агрегации в командной строке, проверки .NET API для распаковки ZIP-файлов или вызова REST API для прояснения того, что он возвращает или как он работает. Вы получаете простые средства для исследования и понимания какого-либо API без издержек появления еще одного CSPROJ-файла в вашем каталоге %TEMP%.

Интерфейс командной строки C# REPL (CSI.EXE)

Как и при изучении самого C#, лучший способ начать освоение интерфейса C# (CSI) REPL — запустить его и приступить к выполнению команд. Для запуска интерфейса, введите команду csi.exe из командной строки Visual Studio 2015 или используйте полный путь C:\Program Files (x86)\MSBuild\14.0\bin\csi.exe. Отсюда начнется выполнение выражений C# по аналогии с тем, что показано на рис. 1.

Рис. 1. Пример CSI REPL

C:\Program Files (x86)\Microsoft Visual Studio 14.0>csi
Microsoft(R) Visual C# Interactive Compiler version 1.1.0.51014
Copyright (C) Microsoft Corporation. All rights reserved.

Type "#help" for more information.
> System.Console.WriteLine("Hello! My name is Inigo Montoya");
Hello! My name is Inigo Montoya
>
> ConsoleColor originalConsoleColor  = Console.ForegroundColor;
> try{
.  Console.ForegroundColor = ConsoleColor.Red;
.  Console.WriteLine("You killed my father. Prepare to die.");
. }
. finally
. {
.  Console.ForegroundColor = originalConsoleColor;
. }
You killed my father. Prepare to die.
> IEnumerable<Process> processes = Process.GetProcesses();
> using System.Collections.Generic;
> processes.Where(process =>
    process.ProcessName.StartsWith("c") ).
.  Select(process => process.ProcessName ).Distinct()
DistinctIterator { "chrome", "csi", "cmd", "conhost", "csrss" }
> processes.First(process => process.ProcessName ==
    "csi" ).MainModule.FileName
"C:\\Program Files (x86)\\MSBuild\\14.0\\bin\\csi.exe"
> $"The current directory is { Environment.CurrentDirectory }."
"The current directory is C:\\Program Files (x86)\\
   Microsoft Visual Studio 14.0."
>

Первым делом отмечу очевидное — он похож на C#, хоть и на новый диалект C# (но без формальностей, которые ценятся в полностью производственной программе и не обязательных в сделанном на скорую руку прототипе). Поэтому, как и следовало ожидать, если вы хотите вызвать какой-нибудь статический метод, то можете написать полное имя метода и передать аргументы внутри скобок. Как и в C#, вы объявляете переменную, предваряя ее типом и (не обязательно) присваивая ей новое значение в момент объявления. И вновь, как и следовало ожидать, нормально работает любой допустимый синтаксис тела метода — блоки try/catch/finally, объявление переменных, лямбда-выражения и LINQ.

И даже в командной строке поддерживаются другие средства C# вроде строковых конструкций (чувствительность к регистру букв, строковые литералы и интерполяция строк). Поэтому, когда вы используете или выводите пути, для каждого обратного слеша нужно ставить escape-символ C# (\), как это делается двойными обратными слешами в пути запуска csi.exe. Интерполяция строк тоже работает, что демонстрирует пример строки «текущего каталога» на рис. 1.

Однако поддержка скриптов в C# позволяет делать гораздо больше, чем просто набирать выражения в командной строке. Вы можете объявлять собственные типы, встраивать метаданные типа через атрибуты и даже упрощать вывод, используя специфичные для скриптов на C# директивы. Рассмотрим пример с проверкой правописания на рис. 2.

Рис. 2. Скриптовый C#-класс Spell (Spell.csx)

#r ".\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll"
#load "Mashape.csx" // задает значение для строки Mashape.Key

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public class Spell
{
  [JsonProperty("original")]
  public string Original { get; set; }
  [JsonProperty("suggestion")]
  public string Suggestion { get; set; }

  [JsonProperty(PropertyName ="corrections")]
  private JObject InternalCorrections { get; set; }

  public IEnumerable<string> Corrections
  {
    get
    {
      if (!IsCorrect)
      {
        return InternalCorrections?[Original].Select(
          x => x.ToString()) ?? Enumerable.Empty<string>();
      }
      else return Enumerable.Empty<string>();
    }
  }
  public bool IsCorrect
  {
    get { return Original == Suggestion; }
  }

  static public bool Check(string word,
    out IEnumerable<string> corrections)
  {
    Task <Spell> taskCorrections = CheckAsync(word);
    corrections = taskCorrections.Result.Corrections;
    return taskCorrections.Result.IsCorrect;

  }
  static public async Task<Spell> CheckAsync(string word)
  {
    HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
      $"https://montanaflynn-spellcheck.p.mashape.com/
      check/?text={ word }");
    request.Method = "POST";
    request.ContentType = "application/json";
    request.Headers = new WebHeaderCollection();
    // Mashape.Key – строковый ключ, доступный
    // Mashape для montaflynn API
    request.Headers.Add("X-Mashape-Key", Mashape.Key);

    using (HttpWebResponse response =
      await request.GetResponseAsync() as HttpWebResponse)
    {
      if (response.StatusCode != HttpStatusCode.OK)
        throw new Exception(String.Format(
        "Server error (HTTP {0}: {1}).",
        response.StatusCode,
        response.StatusDescription));
      using(Stream stream = response.GetResponseStream())
      using(StreamReader streamReader =
        new StreamReader(stream))
      {
        string strsb = await streamReader.ReadToEndAsync();
        Spell spell = Newtonsoft.Json.JsonConvert.
          DeserializeObject<Spell>(strsb);
        // Предполагаем, что проверка правописания
        // была затребована только в первом слове
        return spell;
      }
    }
  }
}

По большей части это просто стандартное объявление C#-класса. Однако здесь есть несколько специфичных для скриптов на C# средств. Во-первых, директива #r служит для ссылки на внешнюю сборку. В данном случае это ссылка на Newtonsoft.Json.dll, которая помогает разбирать JSON-данные. Но заметьте, что эта директива предназначена для ссылок на файлы в файловой системе. Как таковая, она не требует лишних формальностей с escape-символами для обратных слешей.

Во-вторых, вы можете взять весь этот листинг и сохранить его как CSX-файл, а затем «импортировать» или «подставить» (inline) файл в окно C# REPL, используя #load Spell.csx. Директива #load позволяет включать дополнительные файлы скриптов так, будто все #load-файлы были включены в один и тот же «проект» или «компиляцию». Размещение кода в отдельном файле C#-скрипта обеспечивает своего рода рефакторинг файла и, что важнее, возможность хранить C#-скрипт.

Использование объявлений — еще одно языковое средство C#, включенное в скрипты C#; оно несколько раз используется в коде на рис. 2. Заметьте, что, как и в C#, область объявления using ограничена файлом. Поэтому, если вы вызвали #load Spell.csx из окна REPL, объявление using Newtonsoft.Json не сохранится за пределами Spell.csx. Иначе говоря, использование Newtonsoft.Json из Spell.csx не сохранилось бы в окне REPL без его повторного явного объявления в окне REPL (и наоборот). Также заметьте, что поддерживается и объявления using static из C# 6.0. Благодаря этому объявление using static System.Console исключает необходимость предварять любой из членов System.Console типом, разрешая выполнение таких REPL-команд, как WriteLine("Hello! My name is Inigo Montoya").

Другие конструкции, заслуживающие внимания в скриптах на C#, включают атрибуты, выражения using, объявление свойства и функции и поддержку async/await. Учитывая поддержку последнего функционала, await можно использовать даже в окне REPL:

 

(await Spell.CheckAsync("entrepreneur")).IsCorrect

Ниже дан ряд дополнительных замечаний по интерфейсу C# REPL.

  • Запустить csi.exe из Windows PowerShell Integrated Scripting Environment (ISE) нельзя, так как эта программа требует прямого консольного ввода, что не поддерживается имитируемыми консольными окнами Windows PowerShell ISE. (По этой причине подумайте о ее добавлении в список неподдерживаемых консольных приложений $psUnsupportedConsoleApplications.)
  • Команды exit или quit для выхода из CSI-программы нет. Вместо этого используйте Ctrl+C для завершения программы.
  • Журнал команд (command history) сохраняется между сеансами работы с csi.exe, запускаемыми из одного и того же сеанса cmd.exe или PowerShell.exe. Например, если вы запускаете csi.exe, вызываете Console.WriteLine("HelloWorld"), используете Ctrl+C для выхода, а затем снова запускаете csi.exe, нажатие стрелки вверх покажет предыдущую команду: Console.WriteLine("HelloWorld"). Выход из окна cmd.exe с последующим перезапуском очистит журнал команд.
  • Csi.exe поддерживает REPL-команду #help, которая дает вывод, показанный на рис. 3.
  • Csi.exe поддерживает ряд параметров в командной строке, как представлено на рис. 4.

Рис. 3. Вывод REPL-команды #help

> #help
Keyboard shortcuts:
  Enter         If the current submission appears to be
                complete, evaluate it.
                Otherwise, insert a new line.
  Escape        Clear the current submission.
  UpArrow       Replace the current submission
                with a previous submission.
  DownArrow     Replace the current submission
                with a subsequent submission
                (after having previously navigated backward).

REPL commands:
  #help         Display help on available commands
                and key bindings.

Рис. 4. Параметры командной строки csi.exe

Для верстки: в этом листинге не обойтись без наезда на соседнюю колонку, иначе будет сильное искажение вывода

Microsoft (R) Visual C# Interactive Compiler version 1.1.0.51014
Copyright (C) Microsoft Corporation. All rights reserved.

Usage: csi [option] ... [script-file.csx] [script-argument] ...

Executes script-file.csx if specified, otherwise launches an interactive
REPL (Read Eval Print Loop).

Options:
  /help       Display this usage message (alternative form: /?)
  /i          Drop to REPL after executing the specified script
  /r:<file>   Reference metadata from the specified assembly file
              (alternative form: /reference)
  /r:<file list> Reference metadata from the specified assembly files
                 (alternative form: /reference)
  /lib:<path list> List of directories where to look for libraries specified
                   by #r directive (alternative forms: /libPath /libPaths)
  /u:<namespace>   Define global namespace using
                   (alternative forms: /using, /usings, /import, /imports)
  @<file>     Read response file for more options
  --          Indicates that the remaining arguments should not be
              treated as options

Как отмечалось, csi.exe позволяет указывать файл «профиля» по умолчанию, который настраивает ваше окно команд.

  • Чтобы очистить консоль CSI, вызовите Console.Clear. (Подумайте насчет директивы using static System.Console для добавления поддержки вызова просто Clear.)
  • Если вы вводите команду, занимающую несколько строк, и обнаруживаете ошибку на одной из предыдущих строк, то можете использовать Ctrl+Z с последующим нажатием Enter для отмены и возврата к пустой строке приглашения без выполнения (заметьте, что в консоли появится ^Z).

Окно C# Interactive в Visual Studio

Как упоминалось, в Update 1 появилось и новое окно C# Interactive (рис. 5). Это окно запускается через View | Other Windows | C# Interactive и открывается как дополнительное стыкуемое окно. Как и окно csi.exe, это окно C# REPL, но с несколькими дополнительными средствами. Прежде всего оно включает цветовое выделение синтаксиса кода и поддержку IntelliSense. Аналогично компиляция выполняется в реальном времени по мере того, как вы редактируете код, поэтому синтаксические ошибки и другие огрехи автоматически подчеркиваются волнистой красной линией.

Объявление скриптовой функции C# вне класса, использующего окно C# Interactive в Visual Studio
Рис. 5. Объявление скриптовой функции C# вне класса, использующего окно C# Interactive в Visual Studio

Окно C# Interactive, конечно, сразу же ассоциируется с окнами Immediate и Command в Visual Studio. Хотя они перекрываются по своей функциональности (в конце концов, это REPL-окна, в которых вы выполняете .NET-выражения), они имеют существенно разное предназначение. Окно C# Immediate прямо связано с отладочным контекстом вашего приложения, тем самым позволяя вводить дополнительные выражения в контекст, изучать данные в сеансе отладки и даже манипулировать/обновлять данные и отладочный контекст. Аналогично окно Command предоставляет CLI для манипуляций над Visual Studio, включая выполнение различных меню, но не из самих меню, а из окна Command. (Например, выполнение команды View.C#Interactive открывает окно C# Interactive.) В противоположность этому окно C# Interactive позволяет выполнять C#, включая все средства, относящиеся к интерфейсу C# REPL, рассмотренному в предыдущем разделе. Однако окно C# Interactive не имеет доступа к отладочному контексту. Это полностью зависимый сеанс C# без описателей (handles) отладочного контекста или даже Visual Studio. Подобно csi.exe это среда, которая позволяет экспериментировать с фрагментами кода на C# и .NET для проверки вашего понимания без запуска еще одного консольного проекта или проекта модульного тестирования Visual Studio. Но вместо запуска в отдельной программе окно C# Interactive размещается в Visual Studio, где предположительно уже находится разработчик.

Ниже приведено несколько замечаний по окну C# Interactive.

  • Оно поддерживает ряд дополнительных REPL-команд, отсутствующих в csi.exe, в том числе:
    • #cls/#clear для очистки содержимого окна редактора;
    • #reset для восстановления среды выполнения до ее начального состояния с сохранением журнала команд.
  • Клавиатурные комбинации немного неожиданные, как показывает вывод #help в табл. 1.

Табл. 1. Комбинации клавиш, действующие в окне C# Interactive

Enter Если текущее выражение полное, оно оценивается. В ином случае вставляется новая строка
Ctrl+Enter       Оценивается текущее выражение
Shift+Enter      Вставка новой строки
Esc Очистка текущего выражения
Alt+стрелка вверх   Замена текущего выражения предыдущим 
Alt+стрелка вниз Замена текущего выражения последующим (если ранее был переход назад)
Ctrl+Alt+стрелка вверх Замена текущего выражения предыдущим, начинающимся с того же текста
Ctrl+Alt+стрелка вниз Замена текущего выражения последующим, начинающимся с того же текста (если ранее был переход назад)
Стрелка вверх В конце текущего выражения его замена предыдущим. В ином случае перемещение курсора на строку вверх
Стрелка вниз В конце текущего выражения его замена последующим (если ранее был переход назад). В ином случае перемещение курсора на строку вниз
Ctrl+K, Ctrl+Enter Вставка выделенного блока в конец интерактивного буфера; «каретка» остается в в конце ввода
Ctrl+E, Ctrl+Enter Вставка и выполнение выделенного блока до любого незавершенного ввода в интерактивном буфере
Ctrl+A При первом нажатии выделяет выражение, в котором находится курсор. При втором нажатии выделяет весь текст в окне

Важно отметить, что Alt+стрелка вверх/вниз — это комбинации клавиш для вызова журнала команд. Microsoft выбрала эти комбинации вместо более простых «стрелка вверх/вниз», поскольку хотела, чтобы окно Interactive работало так же, как и стандартное окно кода в Visual Studio.

  • Поскольку окно C# Interactive размещается в Visual Studio, в нет тех же возможностей передачи ссылок, использования директив или импорта через командную строку, как в случае csi.exe. Вместо этого окно C# Interactive загружает свой контекст выполнения по умолчанию из C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies\CSharpInteractive.rsp, который идентифицирует сборки для ссылок по умолчанию:
# Этот файл содержит параметры командной строки, которые
# C# REPL будет обрабатывать как часть каждой компиляции,
# если только в команде reset не задан параметр /noconfig
/r:System
/r:System.Core
/r:Microsoft.CSharp
/r:System.Data
/r:System.Data.DataSetExtensions
/r:System.Xml
/r:System.Xml.Linq
SeedUsings.csx

Более того, файл CSharpInteractive.rsp ссылается на файл C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies\SeedUsings.csx по умолчанию:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

Комбинация этих двух файлов является причиной того, что вы используете Console.WriteLine и Environment.CurrentDirectory вместо полных System.Console.WriteLine и System.Environment.CurrentDirectory соответственно. Кроме того, ссылки на сборки, такие как Microsoft.CSharp, позволяют использовать языковые средства вроде dynamic безо всяких дополнительных усилий. (Модификация этих файлов дает возможность изменять ваш «профиль» или «предпочтения» так, чтобы эти изменения сохранялись между сеансами.)

Подробнее о синтаксисе скриптов на C#

Один из моментов, которые следует учитывать в отношении синтаксиса C#-скриптов, заключается в том, что большая часть формальностей, важных в стандартном C#, становится соответственно не обязательной в C#-скрипте. Например, такие вещи, как тела методов не обязаны находиться в какой-либо функции, а функции C#-скрипта можно объявлять вне границ какого-либо класса. Скажем, вы могли бы определить NuGet-функцию Install, которая появляется непосредственно в REPL-окне, как показано на рис. 5. Кроме того, возможно, несколько неожиданно, C#-скрипты не поддерживают объявления пространств имен. Например, вы не можете обернуть класс Spell в пространство имен Grammar: namespace Grammar { class Spell {} }.

Важно отметить, что вы можете объявлять одну и ту же конструкцию (переменную, класс, функцию и т. д.) снова и снова. Последнее объявление следует любому более раннему объявлению.

Другой важный момент, о котором нужно знать, — поведение точки с запятой, завершающей команду. Операторы (например, присваивания переменных) требуют точки с запятой. Без нее REPL-окно будет по-прежнему предлагать дальнейший ввод (через точку), пока не будет введена точка с запятой. С другой стороны, выражения будут выполняться без точки с запятой. Следовательно, System.Diagnostics.Process.Start("notepad") запустит Notepad даже без концевой точки с запятой. Более того, поскольку вызов метода Start возвращает процесс, в командной строке появится строковый вывод выражения: [System.Diagnostics.Process (Notepad)]. Но закрытие выражения точкой с запятой скроет этот вывод. Поэтому вызов Start с концевой точкой с запятой не даст никакого вывода, хотя Notepad все равно будет запущен. Конечно, Console.WriteLine("It would take a miracle."); будет по-прежнему выводить текст, даже без точки с запятой, поскольку вывод отображает сам метод.

Выражения (expressions) и операторы (statements) иногда имеют тонкие отличия. Например, оператор string text = "There’s a shortage of perfect b…."; не даст никакого вывода, а text="Stop that rhyming and I mean it" вернет присвоенную строку (поскольку присваивание возвращает присвоенное значение и в конце нет точки с запятой, которая подавила бы вывод).

Директивы в C#-скриптах для ссылок на дополнительные сборки (#r) и импорт существующих C#-скриптов (#load) — великолепные новшества. К сожалению, на момент написания этой статьи NuGet-пакеты не поддерживались. Для ссылки на файл из NuGet требуется установить этот пакет в каталог, а затем сослаться на конкретную DLL через директиву #r. (В Microsoft меня уверили, что вскоре необходимая поддержка появится.)

Заметьте, что на этот раз директивы ссылаются на конкретные файлы. Указать в директиве, например, какую-то переменную нельзя. Хотя этого и следовало ожидать, это исключает возможность динамической загрузки сборки. Скажем, вы могли бы динамически запускать nuget.exe install для извлечения сборки (вновь см. рис. 5). Однако тогда ваш CSX-файл было бы нельзя динамически связать с извлеченным NuGet-пакетом, так как нет никакого способа динамически передавать путь сборки в директиву #r.

C# CLI

Должен признаться, что у меня двоякое отношение к Windows PowerShell. Мне нравится удобство работы с Microsoft .NET Framework в командной строке и возможность передачи .NET-объектов по конвейеру, а не традиционного текста, как это делается во многих других CLI. Когда дело доходит до языка C#, я становлюсь пристрастен: обожаю его элегантность и мощь. (До сих пор я остаюсь под впечатлением от языковых расширений, которые сделали возможной работу с LINQ.) Поэтому идея сочетания свободы действий в Windows PowerShell .NET с элегантностью языка C# наводила на мысль, что C# REPL может быть заменой Windows PowerShell. После запуска csi.exe я сразу же опробовал команды вроде cd, dir, ls, pwd, cls, alias и им подобные. Достаточно сказать, что я был разочарован, так как ни одна из них не работала. После раздумий и обсуждения с группой C# я осознал, что замена Windows PowerShell не была в числе задач, на которых группа сосредоточилась в версии 1. Более того, это .NET Framework, а значит, она поддерживает расширяемость добавлением ваших собственных функций для предыдущих команд и даже обновлением реализации C#-скрипта в Roslyn. Я немедленно приступил к определению функций для таких команд. Получившаяся в итоге библиотека в своем начальном виде доступна для скачивания на GitHub по ссылке github.com/CSScriptEx.

Для тех, кто ищет более функциональный C# CLI, который изначально поддерживает предыдущий список команд, присмотритесь к ScriptCS на scriptcs.net (или на GitHub по ссылке github.com/scriptcs). Он тоже использует Roslyn и включает поддержку псевдонимов, команд cd, clear, cwd, exit, help, install, reset, ссылок, пакетов скриптов, выражений using и var. Заметьте, что в случае ScriptCS префиксом команд сегодня является двоеточие (как в :reset), а не символ # (как в #reset). В качестве бонуса ScriptCS также добавляет поддержку CSX-файлов с цветовым выделением и IntelliSense в Visual Studio Code.

Заключение

По крайней мере пока целью интерфейса C# REPL не является замена Windows PowerShell или даже cmd.exe. Ожидание этого приведет к разочарованию. Вместо этого я предлагаю относиться к C#-скриптам и различным REPL CLI как к облегченным заменам Visual Studio | New Project: UnitTestProject105 или к аналогам на dotnetfiddle.net. Это способы, ориентированные на C# и .NET, должны углубить ваше понимание языка и .NET API. C# REPL предоставляет средства для кодирования коротких фрагментов или блоков программы, с которыми вы можете поэкспериментировать, пока они не будут готовы для вставки в более серьезные программы. Это позволяет вам писать более содержательные скрипты, чей синтаксис проверяется «на лету» в процессе написания кода (вылавливаются даже такие мелкие ошибки, как неправильный регистр букв), и вас не вынуждают запускать скрипт только для того, чтобы обнаружить, что вы где-то опечатались. Как только вы это поймете, поддержка скриптов на C# и ее интерактивные окна станут приятным инструментом, который вы искали с момента появления первой версии языка.

Как бы ни были интересны C# REPL и поддержка скриптов на C# сами по себе, считайте, что они также являются плацдармом инфраструктуры расширения для ваших приложений — а-ля Visual Basic for Applications (VBA). Благодаря интерактивному окну и поддержке скриптов на C# вы можете вообразить мир (не столь уж и несбыточный), в котором вновь можно добавлять в приложения «макросы», но уже относящиеся к .NET, — без изобретения собственного языка, средства синтаксического анализа и редактора.


Марк Михейлис (Mark Michaelis) — учредитель IntelliTect, где является главным техническим архитектором и тренером. Почти два десятилетия был Microsoft MVP и региональным директором Microsoft с 2007 года. Работал в нескольких группах рецензирования проектов программного обеспечения Microsoft, в том числе C#, Microsoft Azure, SharePoint и Visual Studio ALM. Выступает на конференциях разработчиков, автор множества книг, последняя из которых — «Essential C# 6.0 (5th Edition)» (itl.tc/EssentialCSharp). С ним можно связаться в Facebook (facebook.com/Mark.Michaelis), через его блог (IntelliTect.com/Mark), в Twitter (@markmichaelis) или по электронной почте mark@IntelliTect.com.

Выражаю благодарность за рецензирование статьи экспертам Microsoft Кевину Босту (Kevin Bost) и Кейси Олинхуту (Kasey Uhlenhuth).