Net framework и net core разница
Теперь ознакомимся с историей каждой платформы, чтобы понять различие между ними и их преимущества.
Это приводит к оценке различий API между вертикалями на уровне сборки, а не на уровне отдельных API, которая практиковалась ранее. Этот аспект породил концепцию библиотеки классов, которая может работать с несколькими вертикалями. Такие библиотеки называются переносимыми библиотеками классов (PCL).
Благодаря PCL процесс разработки унифицирован по вертикалям на основе структуры API. Кроме того, разрешается самая актуальная потребность в создании библиотек, работающих в разных вертикалях. Но существует сложная проблема: интерфейсы API переносимы только тогда, когда реализация продвигается по всем вертикалям.
Такая платформа обеспечивает множество преимуществ с точки зрения эффективности и производительности приложений, упрощая упаковку и развертывание на различных поддерживаемых платформах.
Начиная с .NET Core 3.0 помимо уже существующей поддержки Интернета и облака появилась также поддержка настольных компьютеров, IoT и ИИ. Цель этой платформы впечатляет: охватить все типы разработки .NET, как существующие, так и те, которые появятся в будущем. Майкрософт планирует реализовать это видение с помощью .NET 5 в конце 2020 года. Чтобы подчеркнуть уникальность этой платформы в мире .NET, слово "Core" из названия было удалено.
Бенчмарки
Также я прогонял все тесты на двух дополнительных машинах (на Haswell и Sky Lake), чтобы убедиться, что результаты тестов стабильны и воспроизводятся на другом железе.
Цикл for
В Core JIT генерирует более эффективный код, чтение элементов из List в цикле for стало быстрее на ~20%.
Цикл foreach
Итерирование List с ссылочными типами через foreach стало быстрее на 27%, но для значимых типов ничего не поменялось. Здесь можно оценить, насколько foreach медленнее, чем for. Разница в их эффективности на Core составляет 3.5x (value types) и 12x (reference types), примерно также как и в полном фреймворке.
Add
Чтобы протестировать метод без ресайза внутреннего массива в тесте используется конструктор List с заданной ёмкостью (capacity).
На Core 3 добавление работает быстрее на 22% (reference types) и 37% (value types). Что изменилось в коде метода? Добавление без ресайза, т.е. самый частый вариант выделен в отдельный метод с атрибутом [AggressiveInlining] , т.е. он теперь инлайнится. Из мелких оптимизаций: убраны две лишние проверки выхода за границы и значение поля size теперь кешируется в локальную переменную.
Contains
Давайте возьмём негативный сценарий для метода Contains: будем искать элементы, которых нет в коллекции.
На Core 3 поиск Int в List стал примерно в 6 раз быстрее, а поиск строк — в 1.4 раза. В Core JIT научился в некоторых ситуациях девирутализировать виртуальные методы, т.е. они вызываются напрямую. Более того, такие методы могут быть заинлайнены. В данном случае девиртуализируется метод EqualityComparer.Default.Equals , который используется для сравнения элементов. В случае с Int всё сводится к вызову Int32.Equals , который к тому же инлайнится. В итоге получившийся код по эффективности близок к прямому сравнению двух Int.
Кстати, раньше я всегда думал, что метод Contains внутри вызывает IndexOf, но оказалось, что это верно только для Core. В полном фреймворке это разные методы, и работают они с разной скоростью.
List Methods Summary
Сводная таблица относительной производительности (ratio) основных методов List при N = 1000.
Array Methods Summary
Подробно останавливаться на методах массива я не буду, поскольку List — это обертка над массивом.
Так что здесь я приведу таблицу относительной производительности Array при N = 1000.
Здесь можно отметить, что как и прежде, цикл foreach для массива преобразуется в обычный for. Т.е. с точки зрения производительности для итерации массива нет разницы какой из циклов использовать.
Детали переноса компонентов, служб, подсистем
Mono on the Stage
The cross-platform dream had not yet come true.
Tweet This
Директивы препроцессора и условная компиляция
Примеры условных директив, которые приходилось применять на проекте:
Как вы заметили, специально не применялись проверки на конкретные версии runtime'ов для упрощения кода.
Target'ы в *.csproj проектах выглядят так:
→ Больше про Кроссплатформенное нацеливание
Результаты
Как и ожидалось, почти все рассмотренные методы на Core 3 работают быстрее. Разница зачастую составляет 20-30%, а то и больше. Для таких базовых коллекций это отличный результат.
Код и детальные результаты всех тестов доступны на GitHub.
Andrea Chiarelli
Senior Developer Advocate
Last Updated On: October 15, 2021
Andrea Chiarelli
Senior Developer Advocate
Last Updated On: October 15, 2021
However, there are issues are around the corner.
Что имеем
Понятно, что переносить всю систему целиком не имеет смысла — долго и дорого, поэтому можно постепенно переносить только часть подсистем и компонентов до полного исчезновения старых. Проблема в том, что придётся поддерживать обе реализаций в этот переходный период и две реализации будут жить бок о бок. Для этого стараемся рефакторить код так, чтобы он одновременно работал и на старой и на новой реализации.
Для того, чтобы добиться переносимости кода между различными средами исполнения (Framework и Core), нам на помощью приходит NetStandard (а конкретнее — netstandard2.0).
Ещё нужно быть готовым к тому, что часть технологий частично или полностью отсутствует в NetCore, а конкретно:
Старайтесь максимально возможно сохранить неизменные API ваших приложений, а внутреннюю структуру можно менять как угодно, но помните что это может породить множество багов, так что старайтесь применять модульное тестирование. В переходный период вам придётся поддерживать старую и новую реализации.
Материалы по теме
Инфраструктурная часть
К инфраструктурной части приложения относится:
- Хостинг приложения (Реализация служб)
- Логирование
- Обработка ошибок
- Конфигурация
- Загрузочная часть (Bootstrapper)
- Мониторинг
Конфигурация приложений
Конфигурация классических приложений Net Framework основывается на файлах app.config, и web.config, которые представляют из себя XML файлы и API работы с ними: System.Configuration и класс System.Configuration.ConfigurationManager. Например, часто приходится считывать данные из AppSettings и гораздо реже делать свои классы конфигурации в ConfigurationSection.
В NetCore появилось новое API, которое позволяет работать с конфигурацией в различных форматах (JSON, INI, XML) и использовать различные источники (Файлы, Командная строка, Переменные окружения и т. д.)
Как использовать старую и новую реализацию? Ответ прост: ввести абстракцию над конфигурацией. К счастью, в нашей системе уже была абстракция над конфигурацией, что очень сильно облегчило портирование логики чтения конфигурации.
Кстати, в StackOverflow имеется куча вопросов как организовать конфигурацию
Если всё-таки нужно использовать старую реализацию, то имеется NuGet пакет System.Configuration.ConfigurationManager.
NetCore реализация конфигурации более интуитивная и простая. Здесь вы работаете с конфигурацией по конкретному пути в конфигурационном файле, либо как с объектом (и не нужно описывать сложных ConfigurationSection )
Логирование
В старом проекте применялось логирование в EventLog с использованием API из System.Diagnostics , но а так же была абстракция в виде интерфейса ILogger , которая позволяла логировать с различными уровнями messageLevel (Debug, Info, Warning, Error) и указанием категорий. В более новых проектах уже применялся NLog c той же самой абстракцией ILogger .
В NetCore появилось новое универсальное API: Microsoft.Extensions.Logging, которое предоставляет интерфейс ILogger .
Мы же продолжили использовать нашу абстракцию ILogger , потому что она везде, но конкретная реализация уже использует Microsoft.Extensions.Logging.ILogger , а также она легко позволяет подключить и сконфигурировать кучу существующих логеров, например: NLog, log4Net, Serilog и т.д.
Внедрение зависимостей и инверсия управления
В наших проектах использовались IoC-контейнеры Unity, а в более новых — AutoFac, либо вовсе отсутствовали.
В NetCore добавлена абстракция Microsoft.Extensions.DependencyInjection с использованием класса ServiceCollection , которая позволяет регистрировать типы с уровнями:
Также имеется класс IServiceProvider, который обеспечивающий получение нужной регистрации.
- Всегда используйте внедрение зависимостей через конструктор, затем методы или свойства. Если необходимо что-то конструировать в конкретном методе, то можете внедрить какую-нибудь фабрику и создавать что-либо вызывая её методы, саму фабрику регистрируем отдельно и поближе к конфигурации
- Не пробрасывать контекст IoC-контейнера в код (это заставило нас попотеть, чтобы вынести код из классов)
- Сосредоточьте регистрацию в одном месте
Избавляемся от Global Assembly Cache
Очень давно на проекте было принято решение использовать Global Assembly Cache, чтобы приложения не искали сборки и было централизованное место где они лежат.
Net Core не умеет в GAC, поэтому было принято решение написать кастомный AssemblyResolver, который искал бы в заданной директории используя конфигурацию.
XAML Islands
XAML Islands — это набор компонентов, позволяющих разработчикам использовать новые элементы управления Windows 10 (элементы управления XAML UWP) в своих текущих приложениях WPF, Windows Forms и собственных приложениях Win32 (таких как MFC). Вы можете располагать свои "острова" элементов управления XAML UWP где угодно внутри ваших приложений Win32.
Эти острова XAML стали возможны потому, что в Windows 10 версии 1903 появился набор API, которые позволяют размещать содержимое XAML UWP в окнах Win32 с помощью обработчиков окон (HWnds). Обратите внимание, что использовать XAML Islands могут только приложения, работающие в Windows 10 версии 1903 и более поздних версий.
- Элементы управления упаковкой WebView, WebViewCompatible, InkCanvas, MediaPlayerElement и MapControl переносят некоторые элементы управления XAML UWP в элементы управления Windows Forms или WPF, скрывая концепции UWP для их разработчиков.
- Элемент управления WindowsXamlHost для Windows Forms и WPF позволяет загружать в остров XAML другие элементы управления XAML UWP и пользовательские элементы управления, не заключенные в оболочку.
The Need for a Standard
If you need a given platform's APIs, you should use a specific TFM. For example, if you want to use Windows.Forms for your desktop application, you need to specify the net5 . 0 - windows TFM, since this feature is supported on the Windows operating system. Of course, in this case, you know that your code will run only on that specific platform.
On ASP.NET Core, you need to create an API in your Auth0 Management Dashboard and change a few things on your code. To create an API, you need to sign up for a free Auth0 account. After that, you need to go to the API section of the dashboard and click on "Create API". On the dialog shown, you can set the Name of your API as "Books", the Identifier as "http://books.mycompany.com", and leave the Signing Algorithm as "RS256".
After that, you have to add the call to services . AddAuthentication ( ) in the ConfigureServices ( ) method of the Startup class as follows:
In the body of the Configure ( ) method of the Startup class, you also need to add an invocation to app . UseAuthentication ( ) and app . UseAuthorization ( ) as shown below:
Make sure you invoke these methods in the order shown above. It is essential so that everything works properly.
Finally, add the following element to the appsettings . json configuration file:
Note: Replace the placeholders YOUR_DOMAIN and YOUR_AUDIENCE with the actual values for the domain that you specified when creating your Auth0 account and the Identifier you assigned to your API.
Подводим итоги
Доступ ко всем API Windows 10
Dictionary
Randomized Hash
В .NET Core для расчета хешей строк теперь используется рандомизированный алгоритм (Marvin). Т.е. при каждом запуске приложения хеш одной и той же строки будет разным. Это защита от хеш-атак, в частности "hash flooding" (подробнее). Естественно, этот алгоритм медленнее, чем нерандомизированный. Чтобы производительность Dictionary со строковым ключом не просела, внутри него рандомизированный хеш включается только при достижении определённого количества коллизий (сейчас HashCollisionThreshold = 100 ).
Add
Добавление в Dictionary с ключом String стало быстрее на 19%. В случае с Int ключом результат (ratio) зависит от размера: на 100 — 0.95, на 1'000 — 1.09, на 10'000 — 0.93. Отклонения небольшие, возможно, это просто "шум". На других машинах отклонения ещё меньше. Будем считать, что с ключом типа Int добавление элемента происходит примерно с той же скоростью.
GetValue
Получение элемента по строковому ключу стало быстрее на 25%, по Int ключу — на 14%. Однако, здесь есть зависимость от размера Dictionary. Чем меньше размер — тем больше Framework отстает от Core 3 и наоборот. На маленьких размерах Core 3 работает в 1.5 раза быстрей. При достижении размера в 10'000 производительность Core 3 падает до уровня Framework и даже чуть ниже (см. отчеты ниже).
В коде класса Dictionary слишком много изменений, чтобы однозначно сказать, какие из них больше всего повлияли на производительность.
Dictionary Methods Summary
Сводная таблица относительной производительности основных методов Dictionary при N = 1000.
Заворачиваем всё в NuGet пакеты
Для возможности подключения кода на разных рантаймах рекомендую заворачивать ваши сборки в NuGet пакеты, так что будет возможность иметь сразу несколько реализаций одновременно для NetFramework and NetCore.
Andrea Chiarelli
Senior Developer Advocate
Мне стало интересно, какой прирост производительности можно ожидать от Core в самых базовых классах, которые максимально часто используются в коде. Например, коллекции List, Array и Dictionary.
Если вам тоже интересно, как и почему изменилась производительность основных коллекций в Core 3 — прошу под кат!
Dictionary
Randomized Hash
В .NET Core для расчета хешей строк теперь используется рандомизированный алгоритм (Marvin). Т.е. при каждом запуске приложения хеш одной и той же строки будет разным. Это защита от хеш-атак, в частности "hash flooding" (подробнее). Естественно, этот алгоритм медленнее, чем нерандомизированный. Чтобы производительность Dictionary со строковым ключом не просела, внутри него рандомизированный хеш включается только при достижении определённого количества коллизий (сейчас HashCollisionThreshold = 100 ).
Add
Добавление в Dictionary с ключом String стало быстрее на 19%. В случае с Int ключом результат (ratio) зависит от размера: на 100 — 0.95, на 1'000 — 1.09, на 10'000 — 0.93. Отклонения небольшие, возможно, это просто "шум". На других машинах отклонения ещё меньше. Будем считать, что с ключом типа Int добавление элемента происходит примерно с той же скоростью.
GetValue
Получение элемента по строковому ключу стало быстрее на 25%, по Int ключу — на 14%. Однако, здесь есть зависимость от размера Dictionary. Чем меньше размер — тем больше Framework отстает от Core 3 и наоборот. На маленьких размерах Core 3 работает в 1.5 раза быстрей. При достижении размера в 10'000 производительность Core 3 падает до уровня Framework и даже чуть ниже (см. отчеты ниже).
В коде класса Dictionary слишком много изменений, чтобы однозначно сказать, какие из них больше всего повлияли на производительность.
Dictionary Methods Summary
Сводная таблица относительной производительности основных методов Dictionary при N = 1000.
Производительность
Результаты
Как и ожидалось, почти все рассмотренные методы на Core 3 работают быстрее. Разница зачастую составляет 20-30%, а то и больше. Для таких базовых коллекций это отличный результат.
Код и детальные результаты всех тестов доступны на GitHub.
Поддержка Windows Forms и WPF
Поддержка параллельного выполнения и автономные EXE-файлы
После выпуска новой версии .NET Core вы можете обновлять каждое приложение на компьютере по мере необходимости, не беспокоясь о влиянии на другие приложения. Новые версии .NET Core устанавливаются в свои собственные каталоги и существуют "параллельно" друг с другом.
Бенчмарки
Также я прогонял все тесты на двух дополнительных машинах (на Haswell и Sky Lake), чтобы убедиться, что результаты тестов стабильны и воспроизводятся на другом железе.
Цикл for
В Core JIT генерирует более эффективный код, чтение элементов из List в цикле for стало быстрее на ~20%.
Цикл foreach
Итерирование List с ссылочными типами через foreach стало быстрее на 27%, но для значимых типов ничего не поменялось. Здесь можно оценить, насколько foreach медленнее, чем for. Разница в их эффективности на Core составляет 3.5x (value types) и 12x (reference types), примерно также как и в полном фреймворке.
Add
Чтобы протестировать метод без ресайза внутреннего массива в тесте используется конструктор List с заданной ёмкостью (capacity).
На Core 3 добавление работает быстрее на 22% (reference types) и 37% (value types). Что изменилось в коде метода? Добавление без ресайза, т.е. самый частый вариант выделен в отдельный метод с атрибутом [AggressiveInlining] , т.е. он теперь инлайнится. Из мелких оптимизаций: убраны две лишние проверки выхода за границы и значение поля size теперь кешируется в локальную переменную.
Contains
Давайте возьмём негативный сценарий для метода Contains: будем искать элементы, которых нет в коллекции.
На Core 3 поиск Int в List стал примерно в 6 раз быстрее, а поиск строк — в 1.4 раза. В Core JIT научился в некоторых ситуациях девирутализировать виртуальные методы, т.е. они вызываются напрямую. Более того, такие методы могут быть заинлайнены. В данном случае девиртуализируется метод EqualityComparer.Default.Equals , который используется для сравнения элементов. В случае с Int всё сводится к вызову Int32.Equals , который к тому же инлайнится. В итоге получившийся код по эффективности близок к прямому сравнению двух Int.
Кстати, раньше я всегда думал, что метод Contains внутри вызывает IndexOf, но оказалось, что это верно только для Core. В полном фреймворке это разные методы, и работают они с разной скоростью.
List Methods Summary
Сводная таблица относительной производительности (ratio) основных методов List при N = 1000.
Array Methods Summary
Подробно останавливаться на методах массива я не буду, поскольку List — это обертка над массивом.
Так что здесь я приведу таблицу относительной производительности Array при N = 1000.
Здесь можно отметить, что как и прежде, цикл foreach для массива преобразуется в обычный for. Т.е. с точки зрения производительности для итерации массива нет разницы какой из циклов использовать.
Модели приложений
Консольные приложения
Принципиальных изменений в консольных приложениях нет, кроме того что нужно использовать новое API конфигурации, логгирования.
Достаточно поменять *.cproj файл в формат SDK: ну и соответственно использовать
Windows Forms
Начиная с версии NetCore 3.0 появилась возможность запускать приложения Windows Forms, но после перехода на версию Net Core 3.1 часть legacy контроллов было удалено, поэтому
придётся немного переписать приложения.
Вот список контроллов, которые были "выпилены":
- DataGrid и связанные с ним типы. Можно заменить на DataGridView;
- ToolBar. Заменяем на ToolStrip;
- MainMenu. Заменяем на MenuStrip;
- ContextMenu. Заменяем на ContextMenuStrip.
Более подробно про изменения можно почитать Критические изменения в Windows Forms.
На первых этапах существования Windows Forms для NetCore 3.0 отсутствовал дизайнер форм для Visual Studio 2019, поэтому приходилось рисовать GUI в Net Framework, а потом переключаться на NetCore 3.0, но более поздних редакция появилась такая возможность.
Перенос будет чуть сложнее чем Windows Form приложения, но всё-равно всё происходит достаточно безболезненно.
Первое большое изменение — убран бутрстраппер Global.asax и заменён на класс Startup .
ASPNET ASMX переносим на AspNetCore WebAPI
На каждый asmx сервис создаём контроллер WebAPI и на каждый WebMethod создаём
Action POST метод и переносим соответствующий код с реализацией из asmx сервиса. Недостаток заключается в том, что мы полностью отходим от SOAP модели и вам также придётся переписывать клиентов. Если хотите, то можете оформить в виде Rest-служб.
Другой вариант — придётся использовать сторонние библиотеки, которые могут в SOAP , например: SoapCore.
Ещё один вариант — JSON-RPC, имеется куча различных библиотек под NetCore, все они хорошо внедряются в AspNet Core через Middleware.
ASPNET WebApi переносим на AspNetCore WebAPI
Достаточно простая задача, потому что идеология простоя: есть контроллеры и экшны.
шаги почти аналогичные с переносом MVC:
- Global.asax и заменяем на класс Startup
- Настраиваем авторизация и аутентификацию
- Настраиваем Logger, Exception Handler
- Портируем фильтры и т.д.
ASPNET Web Forms переносим на Blazor
Это очень обширная тема, которая требует отдельный статьи, так что не буду сосредотачиваться на деталях портирования, а опишу обзорно.
Почему было решено портировать Web Forms на Blazor:
Для переноса было написано 2 утилиты:
В связи с тем, что у нас интерфейсы достаточно однотипные и простые, нам достаточно легко удалось портировать ASPNET Web Forms приложения.
Избавляемся от WCF
В NetCore есть частичная реализация WCF Client API:
Так как серверная сторона WCF полностью отсутствует в Net Core, то есть несколько вариантов:
- Портируем как AspNetCore WebAPI
- Портируем как AspNetCore gRPC
- Используем стороннюю библиотеку CoreWCF с многими ограничениям
- Портируем как AspNetCore + JSON-RPC
Другой вариант — AspNetCore gRPC: Перенос службы WCF "запрос — ответ" в gRPC унарный RPC
Пример WCF службы:
Пример gRPC контракта в protobuf формате:
Какой вариант реализации — решать вам, но я предпочитаю следующее:
- Все внешние службы реализовать в виде WebAPI в стиле REST, либо JSON-RPC
- Внутренние службы взаимодействуют по gRPC
Избавляемся от Workflow Foundation
Службы на Workflow Foundation полностью отсутствуют (в Microsoft, видимо, поняли что графическое представление службы никому не удобно и проще всё писать кодом), поэтому у вас есть такие варианты:
Лично мы решили просто избавиться от Workflow Foundation, он нам всегда доставлял неудобства и сделали старым добрым кодом. А что же может быть лучше старого доброго кода?
Реализация заглушек API
В редких случаях вам придётся скопировать часть интрерфейсного API, которая отсутствует в NetStandard и NetCore реализации. Например, чтобы избежать большого числа условных директив в коде, пришлось скопировать часть атрибутов WCF, конфигурации, а также некоторые классы в виде заглушек.
Например, в наших контрактах часто используется WCFный атрибут TransactionFlowAttribute , но он будет использоваться, но интерфейсы, на которые он навешивается используется повсеместно, поэтому делаем так:
Andrea Chiarelli
Senior Developer Advocate
Поддержка Windows Forms и WPF
Преимущества открытого кода
Мне стало интересно, какой прирост производительности можно ожидать от Core в самых базовых классах, которые максимально часто используются в коде. Например, коллекции List, Array и Dictionary.
Если вам тоже интересно, как и почему изменилась производительность основных коллекций в Core 3 — прошу под кат!
Читайте также: