Перенос приложений WPF в .NET Core

В этой статье рассматриваются шаги, необходимые для переноса приложения Windows Presentation Foundation (WPF) из .NET Framework в .NET Core 3.0. Если у вас нет приложения WPF, которое нужно перенести, но вы хотите испытать этот процесс, используйте пример приложения Bean Trader, доступный на GitHub. Исходное приложение (для .NET Framework 4.7.2) доступно в папке NetFx\BeanTraderClient. Сначала мы объясним основные шаги, необходимые для переноса приложений, а затем рассмотрим конкретные изменения, применимые к образцу Bean Trader.

Важно!

Документация руководства по классическим приложениям для .NET 6 и .NET 5 (включая .NET Core 3.1) находится в разработке.

Пробное использование помощника по обновлению

Помощник по обновлению .NET — это средство командной строки, которое можно запускать для различных приложений платформы .NET Framework. Это средство поможет вам в обновлении приложений .NET Framework до .NET 5. После запуска средства в большинстве случаев для завершения миграции приложения потребуются дополнительные действия. Средство включает установку анализаторов, которые могут помочь в завершении миграции.

Дополнительные сведения см. в статье Обновление приложения Windows Forms до .NET 5 с помощью помощника по обновлению .NET.

Предварительные требования

Для перехода на .NET Core необходимо сначала:

  1. Изучить и обновить зависимости NuGet:

    1. Обновите зависимости NuGet, чтобы использовать формат <PackageReference>.
    2. Ознакомьтесь с зависимостями NuGet верхнего уровня для совместимости .NET Core или .NET Standard.
    3. Обновите пакеты NuGet до более новых версий.
    4. Используйте Анализатор переносимости .NET для изучения зависимостей .NET.
  2. Перенести файл проекта в новый формат в стиле SDK:

    1. Укажите, следует ли ориентироваться на .NET Core и .NET Framework или только на .NET Core.
    2. Скопируйте соответствующие свойства и элементы файла проекта в новый файл проекта.
  3. Устранить проблемы сборки:

    1. Добавьте ссылку на пакет Microsoft.Windows.Compatibility.
    2. Найдите и исправьте различие на уровне API.
    3. Удалите разделы app.config, отличные от appSettings или connectionStrings.
    4. При необходимости повторно создайте сформированный код.
  4. Протестировать среду выполнения:

    1. Убедитесь, что перенесенное приложение работает правильно.
    2. Берите во внимание исключения NotSupportedException.

Сведения о примере

Эта статья ссылается на пример приложения Bean Trader, поскольку оно использует разнообразные зависимости, аналогичные тем, которые могут иметь реальные приложения WPF. Приложение небольшое, однако задумывалось для повышения уровня по сравнению с "Hello World" с точки зрения сложности. Приложение демонстрирует некоторые проблемы, с которыми пользователи могут столкнуться при переносе реальных приложений. Приложение взаимодействует со службой WCF, поэтому для правильной работы необходимо также запустить проект BeanTraderServer (доступный в том же репозитории GitHub) и убедиться, что конфигурация BeanTraderClient указывает на правильную конечную точку. (По умолчанию в примере предполагается, что сервер работает на том же компьютере в http://localhost:8090, что будет таковым при запуске BeanTraderServer локально.)

Помните, что этот пример приложения предназначен для демонстрации проблем и решений по переносу в .NET Core. Он не предназначен для демонстрации рекомендаций по использованию WPF. На самом деле он намеренно содержит некоторые антишаблоны, чтобы убедиться в том, что во время переноса вы столкнетесь по крайней мере с парой интересных проблем.

Подготовка

Основной проблемой миграции приложения .NET Framework в .NET Core является то, что его зависимости могут работать по-разному или не работать вообще. Миграция намного проще, чем раньше; многие пакеты NuGet теперь нацелены на .NET Standard. Начиная с .NET Core версии 2.0, контактные зоны .NET Framework и .NET Core стали похожи. Несмотря на это, остаются некоторые различия (как в поддержке пакетов NuGet, так и в доступных API-интерфейсах .NET). Первым шагом в переносе является проверка зависимостей приложения и того, что ссылки имеют формат, который легко переносится в .NET Core.

Обновление до ссылок NuGet <PackageReference>

Более старые проекты .NET Framework, как правило, перечисляют зависимости NuGet в файле packages.config. Новый формат файла проекта в стиле пакета SDK ссылается на пакеты NuGet в качестве элементов <PackageReference> в самом файле csproj, а не в отдельном файле конфигурации.

При миграции существует два преимущества использования ссылок в стиле <PackageReference>.

  • Это стиль ссылки NuGet, необходимый для нового файла проекта .NET Core. Если вы уже используете <PackageReference>, эти элементы файла проекта можно скопировать и вставить непосредственно в новый проект.
  • В отличие от файла packages.config, элементы <PackageReference> относятся только к зависимостям верхнего уровня, от которых проект зависит напрямую. Все остальные транзитивные пакеты NuGet будут определены во время восстановления и записаны в автоматически сформированный файл obj\project.assets.json. Это значительно упрощает определение зависимостей проекта, что полезно при определении того, будут ли необходимые зависимости работать в .NET Core.

Первым этапом перенесения приложения .NET Framework в .NET Core является его обновление для использования ссылок NuGet <PackageReference>. Visual Studio упрощает это задание. Просто щелкните правой кнопкой мыши файл packages.config проекта в обозревателе решений Visual Studio, а затем выберите Перенести packages.config в PackageReference.

Upgrading to PackageReference

Откроется диалоговое окно с вычисленными зависимостями NuGet верхнего уровня и вопросом, какие другие пакеты NuGet следует повысить до верхнего уровня. Для примера Bean Trader не нужно, чтобы эти пакеты были пакетами верхнего уровня, поэтому вы можете снять все эти флажки. Затем нажмите кнопку ОК, и файл packages.config будет удален, а элементы <PackageReference> добавятся в файл проекта.

Ссылки в стиле <PackageReference>не хранят пакеты NuGet локально в папке пакетов. Они хранятся глобально для оптимизации. После завершения миграции измените файл csproj и удалите все элементы <Analyzer>, ссылающиеся на анализаторы, которые ранее поступили из каталога ...\packages. Не беспокойтесь; анализаторы будут добавлены в проект, так как у вас по-прежнему есть ссылки на пакет NuGet. Необходимо просто очистить старые элементы packages.config в стиле <Analyzer>.

Проверка пакетов NuGet

Теперь, когда вы видите пакеты NuGet верхнего уровня, от которых зависит проект, можно проверить, доступны ли эти пакеты в .NET Core. Чтобы определить, поддерживает ли пакет .NET Core, просмотрите его зависимости на nuget.org. Эти сведения отображаются в верхней части страницы сведений о пакете, созданном сообществом fuget.org.

При разработке для .NET Core 3.0 должны работать все пакеты, предназначенные для .NET Core или .NET Standard (так как в .NET Core реализована контактная зона .NET Standard). В некоторых случаях конкретная версия используемого пакета не будет нацелена на .NET Core или .NET Standard, но более новые версии будут. В этом случае следует рассмотреть возможность обновления до последней версии пакета.

Вы также можете использовать пакеты, предназначенные для .NET Framework, но это порождает некоторый риск. Использование .NET Core с зависимостями .NET Framework разрешено, так как контактные зоны в .NET Core и .NET Framework похожи, что часто позволяет таким зависимостям работать. Однако если пакет попытается использовать API .NET, который отсутствует в .NET Core, то возникнет исключение времени выполнения. Из-за этого вы должны ссылаться на пакеты .NET Framework, только если другие параметры недоступны, и понимать, что это накладывает тестовую нагрузку.

Если имеются ссылки на пакеты, которые не предназначены для .NET Core или .NET Standard, вам придется подумать о других вариантах:

  • Существуют ли другие похожие пакеты, которые можно использовать вместо этого? Иногда авторы NuGet публикуют отдельные версии ".Core" своих библиотек, специально предназначенные для .NET Core. Пакеты Enterprise Library являются примером сообщества, публикующего альтернативы ".NetCore". В других случаях для .NET Standard доступны более новые пакеты SDK для определенной службы (иногда с разными именами пакетов). Если доступных вариантов нет, можно приступить к использованию пакетов, предназначенных для .NET Framework, учитывая, что при выполнении в .NET Core их необходимо тщательно протестировать.

Пример Bean Trader имеет следующие зависимости NuGet верхнего уровня:

  • Castle.Windsor, версия 4.1.1

    Этот пакет предназначен для .NET Standard 1.6, поэтому он работает на .NET Core.

  • Microsoft.CodeAnalysis.FxCopAnalyzers, версия 2.6.3
    Это мета-пакет, поэтому не сразу видно, какие платформы он поддерживает, однако документация указывает, что его последняя версия (2.9.2) будет работать как для .NET Framework, так и для .NET Core.

  • Nito.AsyncEx версии 4.0.1

    Этот пакет не предназначен для .NET Core, однако его новая версия 5.0 предназначена. Это часто происходит при миграции, поскольку многие пакеты NuGet недавно добавили поддержку .NET Standard, а более старые версии проекта предназначены только для .NET Framework. Если разница в версиях незначительна, обновление до более новой версии выполнить несложно. Поскольку это изменение основного номера версии, вам нужно быть осторожным при обновлении, так как изменения в пакете могут быть критическими. Впрочем, у нас есть план дальнейших действий.

  • MahApps.Metro, версия 1.6.5

    Этот пакет также не предназначен для .NET Core, но имеет более новую предварительную версию (2.0-alpha), которую можно использовать с этой целью. Опять же, вам нужно обратить внимание на наличие критических изменений, однако новый пакет достаточно надежен.

Зависимости NuGet в примере Bean Trader либо предназначены для .NET Standard/.NET Core, либо имеют более новые версии, которые можно использовать, так что здесь вряд ли возникнут блокирующие проблемы.

Обновление пакетов NuGet

Если это возможно, было бы неплохо обновить версии всех пакетов, предназначенных только для .NET Core или .NET Standard, до более поздних версий (с проектом, по-прежнему предназначенным для .NET Framework), чтобы обнаружить и устранить критические изменения на раннем этапе.

Если вы не хотите вносить изменения в существующую версию приложения .NET Framework, с этим можно подождать, пока у вас не появится новый файл проекта, нацеленный на .NET Core. Однако обновление пакетов NuGet до версии, совместимой с .NET Core, заранее упрощает процесс миграции после создания нового файла проекта и уменьшает число различий между версиями приложения .NET Framework и .NET Core.

С помощью примера Bean Trader вы можете легко выполнить необходимые обновления (используя диспетчер пакетов NuGet Visual Studio) с одним исключением: обновление MahApps.Metro версии 1.6.5 до версии 2.0 приводит к критическим изменениям, связанным с API-интерфейсами управления темой и диакритическими знаками.

В идеале приложение будет обновлено для использования более новой версии пакета (поскольку она, скорее всего, будет работать в .NET Core). Однако в некоторых случаях это может оказаться невозможным. В этих случаях не обновляйте MahApps.Metro, поскольку необходимые изменения нетривиальны, и в этом учебнике рассматривается переход на .NET Core 3, а не MahApps.Metro 2. Кроме того, это зависимость .NET Framework с низким риском, поскольку приложение Bean Trader работает только с небольшой частью MahApps.Metro. Чтобы убедиться, что все работает, после завершения миграции, конечно же, потребуется тестирование. Если бы это был реальный сценарий, было бы неплохо подать заявку на отслеживание работы по переходу на MahApps.Metro версии 2.0, так как в результате непроведения миграции теперь остается некий технический долг.

После обновления пакетов NuGet до последних версий группа элементов <PackageReference> в файле проекта примера Bean Trader будет выглядеть следующим образом.

<ItemGroup>
  <PackageReference Include="Castle.Windsor">
    <Version>4.1.1</Version>
  </PackageReference>
  <PackageReference Include="MahApps.Metro">
    <Version>1.6.5</Version>
  </PackageReference>
  <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers">
    <Version>2.9.2</Version>
  </PackageReference>
  <PackageReference Include="Nito.AsyncEx">
    <Version>5.0.0</Version>
  </PackageReference>
</ItemGroup>

Анализ переносимости .NET Framework

Как только вы поймете состояние зависимостей NuGet вашего проекта, следующее, что нужно учитывать — это зависимости API .NET Framework. Средство Анализатор переносимости .NET полезно для понимания того, какие API .NET вашего проекта доступны на других платформах .NET.

Средство поставляется в качестве подключаемого модуля Visual Studio, средства командной строки или в виде простого графического интерфейса, что упрощает его настройку. Дополнительные сведения об использовании анализатора переносимости .NET (порт API) см. в записи блога Перенос настольных приложений в .NET Core. Если вы предпочитаете использовать командную строку, необходимо выполнить следующие действия.

  1. Скачайте Анализатор переносимости .NET, если у вас его еще нет.

  2. Убедитесь, что приложение .NET Framework успешно переносится в сборку (это хорошая идея перед тем, как переходить на другую платформу).

  3. Запустите порт API с такой командной строкой.

    ApiPort.exe analyze -f <PathToBeanTraderBinaries> -r html -r excel -t ".NET Core"
    

    Аргумент -f указывает путь, содержащий двоичные файлы для анализа. Аргумент -r указывает нужный формат выходного файла. Аргумент -t указывает, какая платформа .NET будет анализировать использование API. В этом случае требуется .NET Core.

При открытии HTML-отчета в первом разделе будут перечислены все анализируемые двоичные файлы и процентное соотношение API .NET, которые они используют на целевой платформе. Процент сам по себе не имеет смысла. Более полезным является просмотр отсутствующих интерфейсов API. Для этого либо выберите имя сборки, либо прокрутите вниз до отчетов для отдельных сборок.

Сосредоточьтесь на сборках, исходным кодом которых вы владеете. Например, в отчете Bean Trader ApiPort есть множество двоичных файлов, но большинство из них относятся к пакетам NuGet. Castle.Windsor показывает, что он зависит от некоторых API-интерфейсов System.Web, которые отсутствуют в .NET Core. Это не проблема, потому что ранее вы убедились, что Castle.Windsor поддерживает .NET Core. В пакетах NuGet часто используются разные двоичные файлы для различных платформ .NET, поэтому неважно, использует ли версия .NET Framework из Castle.Windsor System.Web API или нет, при условии, что пакет также нацелен на .NET Standard или .NET Core (что он и делает).

В примере Bean Trader единственным двоичным файлом, который необходимо рассмотреть, является BeanTraderClient, а в отчете показано, что отсутствуют только два API-интерфейса .NET: System.ServiceModel.ClientBase<T>.Close и System.ServiceModel.ClientBase<T>.Open.

BeanTraderClient portability report

Маловероятно, что они станут блокирующими проблемами, потому что клиентские API WCF (в основном) поддерживаются на .NET Core, поэтому для этих центральных API должны быть доступны альтернативные варианты. На самом деле, просматривая контактную зону .NET Core System.ServiceModel(используя https://apisof.net), вы увидите, что вместо этого в .NET Core есть асинхронные альтернативы.

На основании этого отчета и предыдущего анализа зависимостей NuGet можно сделать вывод, что, скорее всего, не должно быть никаких серьезных проблем при переносе примера Bean Trader в .NET Core. Вы готовы к следующему этапу, в котором вы действительно начнете миграцию.

Перенос файл проекта

Если приложение не использует новый формат файла в стиле пакета SDK, потребуется новый файл проекта, предназначенный для .NET Core. Можно заменить существующий файл csproj или, если вы хотите, чтобы существующий проект не был изменен и сохранить его текущее состояние, можно добавить новый файл csproj, предназначенный для .NET Core. Вы можете создать версии приложения для .NET Framework и .NET Core с одним файлом проекта в стиле пакета SDK с помощью многоэлементного нацеливания (указав несколько целевых объектов <TargetFrameworks>).

Чтобы создать новый файл проекта, можно создать новый проект WPF в Visual Studio или использовать команду dotnet new wpf во временном каталоге для создания файла проекта, а затем скопировать его в нужное место или переименовать. Существует также средство, созданное сообществом, CsprojToVs2017, которое может автоматизировать часть переноса файлов проекта. Это средство полезное, однако вмешательство человека все же нужно для просмотра результатов, чтобы убедиться, что все сведения о переносе верны. Одна конкретная область, которую средство не обрабатывает оптимально, — перенос пакетов NuGet из файлов packages.config. Если средство выполняется в файле проекта, который по-прежнему использует файл packages.config для ссылки на пакеты NuGet, он будет автоматически переноситься в элементы <PackageReference>, но будет добавлять элементы <PackageReference> для всех пакетов, а не только для пакетов верхнего уровня. Если вы уже выполнили перенос элементов<PackageReference> с помощью Visual Studio (как было сделано в этом примере), это средство поможет выполнить оставшуюся часть преобразования. Как и рекомендует Скотт Хэнселман (Scott Hanselman) в посте своего блога о переносе CSPROJ-файлов, перенос вручную является информативным и даст лучшие результаты, если у вас есть только несколько проектов, которые нужно перенести. Но если вы переносите десятки или сотни файлов проекта, то в качестве справки можно использовать такие средства, как CsprojToVs2017.

Чтобы создать новый файл проекта для примера Bean Trader, запустите dotnet new wpf во временном каталоге и переместите созданный файл CSPROJ в папку BeanTraderClient и переименуйте его на BeanTraderClient.Core.csproj.

Поскольку новый формат файла проекта автоматически включает файлы C#, RESX и XAML, найденные в каталоге или в папке, файл проекта уже почти завершен. Чтобы завершить перенос, откройте старые и новые файлы проекта параллельно и просмотрите старый, чтобы узнать, нужно ли перенести какие-либо сведения, содержащиеся в нем. В примере Bean Trader в новый проект нужно скопировать следующие элементы.

  • Нужно скопировать свойства <RootNamespace>, <AssemblyName> и <ApplicationIcon>.

  • Также в новый файл проекта необходимо добавить свойство <GenerateAssemblyInfo>false</GenerateAssemblyInfo>, поскольку пример Bean Trader содержит атрибуты уровня сборки (например, [AssemblyTitle]) в файле AssemblyInfo.cs. По умолчанию новые проекты в стиле пакета SDK автоматически создают эти атрибуты на основе свойств в файле CSPROJ. Поскольку вы не хотите, чтобы это произошло в данном случае (атрибуты, которые создаются автоматически, будут конфликтовать с атрибутами из AssemblyInfo.cs), запретите автоматическое создание атрибутов с помощью <GenerateAssemblyInfo>.

  • Несмотря на то что файлы resx автоматически включаются в качестве внедренных ресурсов, другие элементы <Resource>, например изображения, не поддерживаются. Таким образом, скопируйте элементы <Resource> для внедрения изображений и файлов значков. Вы можете упростить ссылки png в одну строку, используя поддержку стандартных масок в новом формате файла проекта: <Resource Include="**\*.png" />.

  • Аналогичным образом элементы <None> включаются автоматически, но по умолчанию они не копируются в выходной каталог. Поскольку проект Bean Trader включает в себя элемент <None>, скопированный в выходной каталог (используя поведения PreserveNewest), для этого файла необходимо обновить автоматически заполняемый элемент <None>.

    <None Update="BeanTrader.pfx">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    
  • Пример Bean Trader включает в себя файл XAML (Default.Accent.xaml) в качестве Content (а не Page), поскольку темы и диакритические знаки, определенные в этом файле, загружаются из файла XAML во время выполнения, а не внедряются в само приложение. Новая система проектов автоматически включает этот файл в качестве <Page>, так как это XAML-файл. Таким образом, вам нужно удалить XAML-файл как страницу (<Page Remove="**\Default.Accent.xaml" />) и добавить его в качестве содержимого.

    <Content Include="Resources\Themes\Default.Accent.xaml">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
    
  • Наконец, добавьте ссылки NuGet, скопировав <ItemGroup> со всеми элементами <PackageReference>. Если вы еще не обновляли пакеты NuGet до версии, совместимой с .NET Core, вы можете сделать это теперь, когда ссылки на пакеты находятся в проекте, специфичном для .NET Core.

На этом этапе необходимо добавить новый проект в решение BeanTrader и открыть его в Visual Studio. Проект должен выглядеть правильно в обозревателе решений, а dotnet restore BeanTraderClient.Core.csproj — успешно восстанавливать пакеты (с двумя ожидаемыми предупреждениями, связанными с используемой версией MahApps.Metro, нацеленной на .NET Framework).

Хотя оба файла проекта можно сохранить параллельно (и это даже может быть желательно, если вы хотите сохранить сборку старого проекта в том виде, в каком он был), это усложняет процесс переноса (оба проекта будут пытаться использовать одни и те же папки bin и obj), и обычно в этом нет необходимости. Если вы хотите выполнить сборку для .NET Core и .NET Framework, в новом файле проекта свойство <TargetFramework>netcoreapp3.0</TargetFramework> можно заменить на <TargetFrameworks>netcoreapp3.0;net472</TargetFrameworks>. Для примера Bean Trader удалите старый файл проекта (BeanTraderClient.csproj), так как он больше не нужен. Если вы предпочитаете хранить оба файла проекта, не забудьте создать их в разных выходных и промежуточных путях выходных данных.

Устранение проблем сборки

Третьим шагом процесса переноса является получение проекта для сборки. После преобразования файла проекта в проект типа SDK некоторые приложения уже будут успешно собраны. Если это касается вашего приложения, наши поздравления! Вы можете перейти к шагу 4. Для сборки других приложений для .NET Core потребуются некоторые обновления. Если вы, например, попробуете запустить dotnet build в примере проекта Bean Trader сейчас (или собрать его в Visual Studio), ошибок будет много, но вы их быстро исправите.

Ссылки на System.ServiceModel и Microsoft.Windows.Compatibility

Общим источником ошибок является отсутствие ссылок на API, которые доступны для .NET Core, но не включаются в метапакет приложения .NET Core автоматически. Чтобы это решить, необходимо сослаться на пакет Microsoft.Windows.Compatibility. Пакет совместимости включает широкий набор интерфейсов API, которые являются общими в классических приложениях Windows, таких как клиент WCF, службы каталогов, реестр, настройка, API ACL и многое другое.

В примере Bean Trader большинство ошибок сборки вызваны отсутствием типов System.ServiceModel. Эти вопросы можно решить, обратившись к необходимым пакетам WCF NuGet. Клиентские API-интерфейсы WCF существуют в пакете Microsoft.Windows.Compatibility, поэтому обращение к пакету совместимости является еще лучшим решением (поскольку в нем также рассматриваются любые проблемы, связанные с API, а также решения проблем WCF, которые предоставляют пакет совместимости). Пакет Microsoft.Windows.Compatibility помогает в большинстве сценариев переноса WPF и WinForms в .NET Core 3.0. После добавления ссылки NuGet в Microsoft.Windows.Compatibilityостается только одна ошибка сборки!

Очистка неиспользуемых файлов

Один тип проблем с переносом, который часто возникает, связан с C# и файлами XAML, которые ранее не были включены в сборку, а теперь предоставляются новыми проектами в стиле SDK, которые автоматически включают в себя весь источник.

Следующая ошибка сборки, отображаемая в примере Bean Trader, относится к неправильной реализации интерфейса в OldUnusedViewModel.cs. Имя файла является указанием, но при проверке вы обнаружите, что этот исходный файл неверен. Это не вызывало проблем ранее, поскольку файл не включался в исходный проект .NET Framework. Исходные файлы, которые содержались на диске, но не включались в старый csproj, теперь включены автоматически.

Для таких однократных проблем файл легко сравнить с предыдущим csproj, чтобы подтвердить, что файл не нужен, а затем либо <Compile Remove="" /> его, либо, если исходный файл больше нигде не нужен, удалить его. В этом случае можно просто удалить OldUnusedViewModel.cs.

Если у вас есть много исходных файлов, которые необходимо исключить таким образом, можно отключить автоматическое включение файлов C#, задав для свойства <EnableDefaultCompileItems> значение false в файле проекта. Затем вы можете скопировать элементы <Compile Include> из старого файла проекта в новый, чтобы собрать только те исходные тексты, которые вы собирались включить. Аналогичным образом для отключения автоматического включения XAML-страниц можно использовать <EnableDefaultPageItems> и <EnableDefaultItems> может управлять и тем, и другим с помощью одного свойства.

Краткий обзор компиляторов с несколькими проходами

После удаления из примера Bean Trader файла, вызвавшего ошибку, можно выполнить повторную сборку и получить четыре ошибки. Не делали это раньше? Почему количество ошибок увеличилось? Компилятор C# — это компилятор с несколькими проходами. Это означает, что он проходит через каждый исходный файл дважды. Во-первых, компилятор просто просматривает метаданные и объявления в каждом исходном файле и определяет все проблемы на уровне объявления. Вы уже исправили эти ошибки. Затем он снова проходит через код, чтобы собрать источник на C# в IL; это второй набор ошибок, который вы видите сейчас.

Примечание

Компилятор C# делает более двух проходов, но в итоге ошибки компилятора при таких больших изменениях кода, как это, как правило, поступают в две волны.

Исправления зависимостей сторонних производителей (Castle.Windsor)

Еще один класс проблем, которые поступают в некоторых сценариях миграции, — различия в API между версиями зависимостей .NET Framework и .NET Core. Даже если пакет NuGet предназначен как для .NET Framework, так и .NET Standard или .NET Core, для разных библиотек может понадобиться использовать различные целевые объекты .NET. Это позволяет пакетам поддерживать множество различных платформ .NET, которые могут потребовать различных реализаций. Это также означает, что при использовании различных платформ .NET могут возникнуть небольшие различия в библиотеках API.

Следующий набор ошибок, которые вы увидите в примере Bean Trader, связан с API Castle.Windsor. Проект .NET Core Bean Trader использует ту же версию Castle.Windsor, что и проект .NET Framework (4.1.1), но реализации этих двух платформ немного отличаются.

В этом случае вы увидите следующие проблемы, которые необходимо устранить:

  1. Castle.MicroKernel.Registration.Classes.FromThisAssembly недоступен в .NET Core. Однако существует похожий API Classes.FromAssemblyContaining и мы можем заменить оба использования Classes.FromThisAssembly() на вызовы Classes.FromAssemblyContaining(t), где t — это тип, выполняющий вызов.
  2. Аналогичным образом в Bootstrapper.cs, Castle.Windsor.Installer.FromAssembly, который недоступен в .NET Core. Вместо этого вызов можно заменить на FromAssembly.Containing(typeof(Bootstrapper)).

Обновление использования клиента WCF

Устраняя различия в Castle.Windsor, последняя оставшаяся ошибка сборки в проекте .NET Core Bean Trader заключается в том, что BeanTraderServiceClient (который является производным от DuplexClientBase) не имеет метода Open. Это не удивительно, поскольку это API, который был выделен Анализатором переносимости .NET в начале процесса миграции. Однако, глядя на BeanTraderServiceClient, мы обращаем внимание на большую проблему. Этот клиент WCF был автоматически создан средством Svcutil.exe.

Клиенты WCF, созданные Svcutil, предназначены для использования в .NET Framework.

Решения, использующие клиенты WCF, созданные с помощью svcutil, должны повторно создавать совместимые с .NET Standard клиенты для использования с .NET Core. Одна из основных причин, по которой старые клиенты не работают, заключается в том, что они зависят от конфигурации приложения для определения привязок и конечных точек WCF. Поскольку API-интерфейсы WCF .NET Standard могут работать на разных платформах (когда API System.Configuration недоступны), клиенты WCF для сценариев .NET Core и .NET Standard должны определять привязки и конечные точки программно, а не в конфигурации.

Фактически любое использование клиента WCF, которое зависит от раздела <system.serviceModel> app.config (созданного с помощью Svcutil или вручную), необходимо изменить для работы в .NET Core.

Существует два способа автоматического создания совместимых с .NET Standard клиентов WCF:

  • Средство dotnet-svcutil — это средство .NET, которое создает клиенты WCF таким же образом, как ранее работал Svcutil.
  • Visual Studio может создавать клиенты WCF с помощью параметра WCF Web Service Reference его функции Подключенных служб.

Любой из этих подходов хорошо работает. Конечно же, код клиента WCF можно написать самостоятельно. В этом примере я решил использовать функцию Подключенной службы Visual Studio. Для этого щелкните правой кнопкой мыши проект BeanTraderClient.Core в обозревателе решений Visual Studio и выберите Добавить>Подключенная служба. Затем выберите Microsoft WCF Web Service Reference Provider. Откроется диалоговое окно, в котором можно указать адрес внутренней веб-службы Bean Trader (localhost:8080, если сервер выполняется локально) и пространство имен, которое должны использовать создаваемые типы (например, BeanTrader.Service).

WCF Web Service Reference Connected Service Dialog

После нажатия кнопки Готово в проект добавляется новый узел "Подключенные службы", а в этот узел добавляется файл Reference.cs, содержащий новый клиент WCF .NET Standard для доступа к службе Bean Trader. Если взглянуть на методы GetEndpointAddress или GetBindingForEndpoint в этом файле, вы увидите, что привязки и конечные точки теперь создаются программно (вместо использования конфигурации приложения). Функция "Add Connected Services" (Добавить Подключенные службы) также может добавлять в файл проекта ссылки на некоторые пакеты System.ServiceModel, которые не нужны, так как все необходимые пакеты WCF включены через Microsoft.Windows.Compatibility. Проверьте csproj, чтобы узнать, добавлены ли какие-либо дополнительные элементы System.ServiceModel <PackageReference>, и если да, удалите их.

Наш проект теперь содержит новые клиентские классы WCF (в Reference.cs), но по-прежнему также содержит старые (в BeanTrader.cs). На этом этапе есть два варианта:

  • Если вы хотите создать исходный проект .NET Framework (наряду с новым целевым объектом .NET Core), можно использовать элемент <Compile Remove="BeanTrader.cs" /> в файле csproj проекта .NET Core, чтобы в версии приложения .NET Framework и в .NET Core использовались разные клиенты WCF. Это имеет преимущество, поскольку оставляет существующий проект .NET Framework без изменений, но и недостаток, заключающийся в том, что код, использующий сгенерированные клиенты WCF, может использоваться несколько иначе в случае .NET Core, чем это было в проекте .NET Framework, так что для условной компиляции некоторого использования клиента WCF (создание клиентов, например), скорее всего, потребуется использовать директивы #if, чтобы работать в одном направлении при сборке для .NET Core и в другом — при сборке для .NET Framework.

  • Если, с другой стороны, некоторые изменения кода в существующем проекте .NET Framework приемлемы, вы можете удалить BeanTrader.cs полным составом. Поскольку новый клиент WCF создан для .NET Standard, он будет работать в сценариях .NET Core и .NET Framework. При создании для .NET Framework в дополнение к .NET Core (для нескольких версий или с помощью двух файлов csproj) этот новый файл Reference.cs можно использовать для обоих целевых объектов. Преимущество такого подхода заключается в том, что код не будет нуждаться в бифуркации для поддержки двух различных клиентов WCF; один и тот же код будет использоваться везде. Недостаток заключается в том, что он включает изменение (предположительно стабильного) проекта .NET Framework.

В случае с примером Bean Trader можно внести небольшие изменения в исходный проект, если это упростит перенос, поэтому выполните следующие действия для согласования использования клиента WCF:

  1. Добавьте новый файл Reference.cs в проект .NET Framework BeanTraderClient.csproj с помощью контекстного меню "Добавить существующий элемент" в обозревателе решений. Не забудьте добавить "в качестве ссылки", чтобы один и тот же файл использовался обоими проектами (в отличие от копирования файла C#). При создании для .NET Core и .NET Framework с одним csproj (с использованием настройки для различных версий) этот шаг не требуется.

  2. Удалите BeanTrader.cs.

  3. Новый клиент WCF аналогичен старому, но некоторые пространства имен в созданном коде отличаются. Поэтому необходимо обновить проект таким образом, чтобы типы клиента WCF использовались из BeanTrader.Service (или любого выбранного имени пространства имен) вместо BeanTrader.Model или без пространства имен. Сборка BeanTraderClient.Core.csproj поможет определить, где необходимо внести эти изменения. Исправления понадобятся как в C#, так и в исходных файлах XAML.

  4. Наконец, вы обнаружите ошибку в BeanTraderServiceClientFactory.cs, поскольку доступные конструкторы для типа BeanTraderServiceClient изменились. Он использовался для предоставления аргумента InstanceContext (который был создан с помощью CallbackHandler из контейнера Castle.Windsor IoC). Новые конструкторы создают новые CallbackHandler. Однако в базовом типеBeanTraderServiceClient есть конструкторы, соответствующие тем, которые вам нужны. Так как автоматически сформированный клиентский код WCF существует в разделяемых классах, его можно легко расширить. Для этого создайте новый файл с именем BeanTraderServiceClient.cs, а затем — разделяемый класс с тем же именем (с помощью пространства имен BeanTrader.Service). Затем добавьте один конструктор к разделяемому типу, как показано здесь.

    public BeanTraderServiceClient(System.ServiceModel.InstanceContext callbackInstance) :
        base(callbackInstance, EndpointConfiguration.NetTcpBinding_BeanTraderService)
            { }
    

После внесения этих изменений в примере Bean Trader будет использоваться новый клиент WCF, совместимый с .NET Standard, и вы сможете внести окончательное исправление изменения вызова Open в TradingService.cs, чтобы вместо этого использовать await OpenAsync.

С учетом проблем, устраняемых WCF, версия .NET Core примера Bean Trader теперь будет построена четко.

Тестирование среды выполнения

Легко забыть, что работа по переносу не завершается после четкого построения проекта на основе .NET Core. Очень важно оставить время для тестирования приложения. После успешного построения убедитесь, что приложение не просто работает, а работает должным образом, особенно если вы используете пакеты, предназначенные для .NET Framework.

Давайте попробуем запустить перенесенное приложение Bean Trader и посмотрим, что произойдет. Приложение почти сразу завершается ошибкой со следующим исключением.

System.Configuration.ConfigurationErrorsException: 'Configuration system failed to initialize'

Inner Exception
ConfigurationErrorsException: Unrecognized configuration section system.serviceModel.

В этом, конечно, есть смысл. Помните, что WCF больше не использует конфигурацию приложения, поэтому старую секцию system.serviceModel файла app.config необходимо удалить. Обновленный клиент WCF включает в себя всю ту же информацию в своем коде, поэтому раздел конфигурации больше не нужен. Если вы хотите, чтобы конечная точка WCF была настроена в файле app.config, добавьте ее в качестве параметра приложения и обновите код клиента WCF, чтобы получить конечную точку службы WCF из конфигурации.

После удаления раздела system.serviceModel app.config приложение запускается, но завершается с другим исключением при входе пользователя в систему.

System.PlatformNotSupportedException: 'Operation is not supported on this platform.'

Неподдерживаемый API — Func<T>.BeginInvoke. Как описано в dotnet/corefx#5940, .NET Core не поддерживает методы BeginInvoke и EndInvoke в типах делегатов из-за зависимостей с базовым удаленным взаимодействием. Эта проблема и ее исправление более подробно описаны в записи блога Перенос вызовов Delegate.BeginInvoke для .NET Core, но именно в этом случае вызовы BeginInvoke и EndInvoke следует заменить на Task.Run (или асинхронные альтернативы, если это возможно). Применяя общее решение, вызов BeginInvoke можно заменить на вызов Invoke, запускаемый Task.Run.

Task.Run(() =>
{
    return userInfoRetriever.Invoke();
}).ContinueWith(result =>
{
    // BeginInvoke's callback is replaced with ContinueWith
    var task = result.ConfigureAwait(false);
    CurrentTrader = task.GetAwaiter().GetResult();
}, TaskScheduler.Default);

После удаления использования BeginInvoke приложение Bean Trader успешно работает в .NET Core!

Bean Trader running on .NET Core

Все приложения отличаются друг от друга, поэтому конкретные шаги, необходимые для миграции собственных приложений на .NET Core, будут отличаться. Но надеюсь, что в примере Bean Trader продемонстрирован общий рабочий процесс и типы проблем, которые можно ожидать. И несмотря на большой размер этой статьи, реальные изменения, необходимые в примере Bean Trader, чтобы он работал на .NET Core, были довольно незначительными. Переход многих приложений в .NET Core происходит таким же образом; с небольшими изменениями кода или даже без них.