В последние годы команда .NET усиленно рекламирует ASP.NET Core как один из самых быстрых веб-фреймворков на рынке. Источником этих утверждений всегда были бенчмарки TechEmpower Framework Benchmarks.
Возьмем этот слайд с BUILD 2021, который Скотт Хантер - директор по управлению программами .NET - представил в прошлом году:
По его словам, .NET более чем в 10 раз быстрее, чем Node.js.
Скотт также утверждает, что .NET быстрее, чем Java, Go и даже C++, что является огромным похвальством, если это правда!
Совсем недавно Себастьен Рос из команды ASP.NET Core написал следующее на Reddit:
Если говорить только о производительности, то NodeJs даже не стоит в одном ряду с Go и .NET, которые намного быстрее во всех условиях (gRPC, пропускная способность, использование памяти, размер контейнера). Затем для Go это во многом зависит от выбранного вами веб-фреймворка, например Fiber, Gin, .... Проверьте бенчмарки TechEmpower, чтобы почувствовать разницу. Наконец, даже с самым быстрым веб-фреймворком Go, .NET все равно быстрее при использовании стека высокого уровня (middleware, minimal APIs, ...). При этом .NET 7.0 снова быстрее, чем текущие результаты, которые получены на версии 6.0.
Тогда, вероятно, эти две версии превзойдут ваши потребности и обе будут достаточны для того, что вы хотите сделать. Слой базы данных может быть вашим узким местом, что сделает выбор веб-фреймворка спорным вопросом. Инструментарий или уверенность в платформе могут стать вторым лучшим отличительным фактором. Если вам нужна только производительность, то обратите внимание на фреймворки Rust, которые сейчас еще быстрее.
В частности, это предложение было очень интересно прочитать:
Наконец, даже с самым быстрым веб-фреймворком Go, .NET все равно быстрее при использовании стека высокого уровня (middleware, minimal APIs, ...).
Это смелое заявление и столь же впечатляющее, если оно соответствует действительности, поэтому мне было интересно узнать больше о производительности ASP.NET Core и TechEmpower Framework Benchmarks.
TechEmpower Benchmarks
TechEmpower - это программное агентство, располагающееся в Лос-Анджелесе, Калифорния, которое проводит независимые бенчмарки фреймворков на своих серверах. Они публикуют все результаты на своем сайте, также как и код фреймворков на GitHub.
Первое, что бросилось мне в глаза, это то, что последний официальный раунд (Round 21) был зафиксирован 19 июля 2022 года. Предыдущий раунд (Round 20) был проведен в феврале 2021 года, то есть разница между этими двумя официальными раундами больше года. Я не знаю точно, почему у них так мало официальных раундов, но я обнаружил, что у них есть непрерывный бенчмарк-запуск, который можно посмотреть на их панели результатов. Однако, поскольку последний официальный раунд был проведен не так давно, а разница между результатами 21-го раунда и последнего завершенного раунда непрерывного бенчмарка не так уж велика, я решил придерживаться 21-го раунда для дальнейшего анализа.
TechEmpower делит свои тесты на следующие категории:
-
JSON serializers (JSON сериализаторы)
-
Single query (Одиночный запрос)
-
Multiple queries (Множественные запросы)
-
Cached queries (Кэшированные запросы)
-
Fortunes (Фортуны)
-
Data updates (Обновление данных)
-
Plaintext (Обычный текст)
Бенчмарк Fortunes является золотым стандартом всех бенчмарков. Он единственный, кто пытается походить на "реальный сценарий", включающий чтение из базы данных, сортировку данных по тексту, защиту от XSS, а также включает в себя отрисовку HTML-шаблонов на стороне сервера.
Все остальные категории тестов фокусируются на отдельных аспектах фреймворка, что делает их интересными для чтения, но бесполезными при ранжировании веб-фреймворков по общей производительности.
Итак, давайте более подробно рассмотрим бенчмарк Fortunes из 21-го раунда:
К моему удивлению, ASP.NET Core занимает 9-е место среди 10 самых быстрых фреймворков! Два других варианта бенчмарка ASP.NET Core заняли 13-е и 14-е места из 439 завершенных прогонов бенчмарков. Это действительно очень впечатляет!
В чем различия этих ASP.NET Core бенчмарков?
Почему ASP.NET Core появляется несколько раз в результатах бенчмарков с различными показателями производительности?
Оказывается, существует 15 различных бенчмарков ASP.NET Core, которые можно разделить на четыре категории:
-
ASP.NET Core stripped naked *раздетый догола*
-
ASP.NET Core с middleware
-
ASP.NET Core MVC
-
ASP.NET Core на Mono
Однако это самостоятельно выбранные названия (командой .NET), и для того, чтобы получить реальное представление о том, что именно тестируется, необходимо посмотреть на сам код. К счастью, весь код находится в открытом доступе на GitHub.
Мне не интересно проверять 15 различных реализаций различных бенчмарков ASP.NET Core, поэтому я решил сосредоточиться на наиболее эффективных из них, сократив 15 бенчмарков до 7 лучших:
Я удалил бенчмарки Mono и все тесты, которые использовали MySQL в качестве исходной базы данных, потому что эти тесты показали наиболее худшие результаты по сравнению с эквивалентами .NET Core с Postgres (которые имеют суффикс pg в обозначениях).
Постепенно картина становится более ясной. Приведенный выше скриншот также включает "классификацию" фреймворка, которую можно увидеть в правой части изображения. Верхний бенчмарк (который занимает 9 место в общем рейтинге) классифицируется как "Platform". Следующие три бенчмарка классифицируются как "Micro", а последние три бенчмарка - как "Full". Похоже, что при переходе от тестов "Platform" к тестам "Full" наблюдается очень значительное падение производительности.
Аналогично наименованию бенчмарков фреймворка, классификация не стандартизирована и не проверяется сотрудниками TechEmpower. Любой может отправить код с произвольным названием и классификацией и получить очень мало или вообще не получить никакой проверки со стороны владельцев репозитория. По крайней мере, у меня сложилось такое впечатление (однажды я прислал бенчамарк-тест на F#).
Только сам код может быть использован как надежный источник правды, чтобы сделать выводы из этих тестов.
К счастью, код для всех бенчмарков ASP.NET Core (на .NET Core) можно найти в папке /frameworks/CSharp/aspnetcore репозитория GitHub.
19 июля 2022 года (когда проходил 21 раунд) бенчмарк ASP.NET Core был разделен на два проекта:
Оба этих веб-приложения очень разные, поэтому важно понять, какое из них используется в каком бенчмарке. Это можно сделать, изучив файл config.toml
и связанный с ним Dockerfile
для соответствующего тестового примера.
Например, бенчмарк ASP.NET Core (aspcore-ado-pg
), занимающий первое место в рейтинге, имеет следующую конфигурацию:
config.toml
[ado-pg]
urls.db = "/db"
urls.query = "/queries/"
urls.fortune = "/fortunes"
urls.cached_query = "/cached-worlds/"
approach = "Realistic"
classification = "Platform"
database = "Postgres"
database_os = "Linux"
os = "Linux"
orm = "Raw"
platform = ".NET"
webserver = "Kestrel"
versus = "aspcore-ado-pg"
aspcore-ado-pg.dockerfile
FROM mcr.microsoft.com/dotnet/sdk:6.0.100 AS build
WORKDIR /app
COPY PlatformBenchmarks .
RUN dotnet publish -c Release -o out /p:DatabaseProvider=Npgsql
FROM mcr.microsoft.com/dotnet/aspnet:6.0.0 AS runtime
ENV ASPNETCORE_URLS http://+:8080
# Full PGO
ENV DOTNET_TieredPGO 1
ENV DOTNET_TC_QuickJitForLoops 1
ENV DOTNET_ReadyToRun 0
WORKDIR /app
COPY --from=build /app/out ./
COPY PlatformBenchmarks/appsettings.postgresql.json ./appsettings.json
EXPOSE 8080
ENTRYPOINT ["dotnet", "PlatformBenchmarks.dll"]
Dockerfile сообщает нам, что в этом тесте используется код /PlatformBenchmakrs
:
COPY PlatformBenchmarks .
Из файла config.toml
мы можем понять, что тест Fortune вызывает эндпоинт /fortunes
во время выполнения бенчмарка.
Также команда .NET указала в файле config.toml
, что этот конкретный бенчмарк должен быть классифицирован как реалистичный подход:
approach = "Realistic"
Бенчмарк "ASP.NET Core Platform"
Круто, так что же находится внутри этого высокопроизводительного реалистичного приложения ASP.NET Core?
С первого взгляда я не узнал многого из того, что обычно считается типичным приложением ASP.NET Core.
Единственное, что показалось немного знакомым, - это использование Kestrel (веб-сервера .NET) внутри Program.cs:
К моему удивлению, это была единственная вещь, которую я смог распознать как "ASP.NET Core". Само веб-приложение даже не инициализируется с помощью одной из многочисленных идиом ASP.NET Core. Вместо этого оно создает пользовательское приложение BenchmarkApplication
в качестве слушателя на настроенный эндпоинт.
Неподготовленный глаз может подумать, что builder.UseHttpApplication()
- это метод, который поставляется с Kestrel, но это не так. Метод расширения, а также класс HttpApplication
, который здесь используется, не являются тем, что можно найти в реальном фреймворке ASP.NET Core. Это еще один пользовательский класс, специально написанный для этого бенчмарка:
Даже интерфейс IHttpApplication
пришел не из ASP.NET Core. Это также пользовательский тип, который был специально разработан для бенчмарк-тестов.
Заглянув дальше в файл BenchmarkApplication.cs
, я был потрясен огромным количеством тонко настроенного низкоуровневого кода C#, созданного специально для этого (чрезвычайно простого) приложения.
Все, что находится в папке /PlatformBenchmarks
, является пользовательским кодом, который вы не найдете нигде в официальном пакете ASP.NET Core.
Хорошим примером является класс AsciiString
, который используется для статической инициализации огромных фрагментов ожидаемых HTTP-ответов заранее:
Несмотря на то, что он называется AsciiString
, он является строкой только по названию:
В действительности класс AsciiString
- это просто причудливая (высоко оптимизированная) обертка вокруг массива байтов, который преобразует строку в байты во время инициализации. В случае теста Fortunes весь HTTP-заголовок (который приложение должно вернуть во время выполнения теста) создается заранее во время запуска приложения и затем хранится в памяти на протяжении всего теста:
Предполагается, что это очень простое приложение, которое фреймворк, вероятно, мог бы втиснуть в один файл кода, но проект /PlatformBenchmarks
содержит многие десятки искусно созданных классов со всевозможными хитростями, применяемыми для достижения желаемого результата.
Мера, на которую пошла команда .NET, необычайна.
ASP.NET Core имеет множество способов реализации маршрутизации. У них есть Actions и Controllers, маршрутизация эндпоинтов, minimal API, или если кто-то хочет работать на самом низком уровне ASP.NET Core (= Platform), то он может работать непосредственно с объектами Request и Response из HttpContext.
Ни один из этих вариантов не может быть найден в /PlatformBenchmarks
:
На самом деле, вы вообще нигде не найдете HttpContext. Такое впечатление, что команда .NET пыталась избежать использования ASP.NET Core любой ценой, что, по меньшей мере, странно.
Просеивание проекта выявило еще более странный код, который команда .NET применила, чтобы "подправить" результаты бенчмарка.
Например, взгляните на реализацию шаблонов HTML в решении ASP.NET Core:
HTML-шаблона нет вообще. Весь смысл бенчмарка Fortunes заключается, помимо прочего, в тестировании различных веб-фреймворков на скорость вывода шаблонизированного HTML. В ASP.NET Core у нас есть два механизма шаблонизации, Razor Views и Razor Pages, ни один из которых здесь не используется.
Вместо этого здесь больше жестко закодированных статически инициализированных массивов байтов:
Конечно, остается вопрос, разрешены ли подобные трюки? Границы могут быть немного размыты, но я уверен, что эта реализация раздвигает границы того, что можно считать настоящим шаблонизатором.
Веб-фреймворки не обязаны участвовать в каждой категории тестов TechEmpower Benchmark. На самом деле рекомендуется участвовать только в тех категориях, которые применимы к конкретному фреймворку. Например, если реализация ASP.NET Core низкого уровня (реальная реализация, использующая ASP.NET Core с HttpContext
и так далее) не имеет включенного рендеринга шаблонов, то она не должна участвовать в бенчмарке Fortunes. Если фреймворк более высокого уровня, такой как ASP.NET Core MVC, имеет возможность рендеринга шаблонов HTML, то он может участвовать в бенчмарке Fortunes. Участие в бенчмарке Fortunes со случайным кодом на C#, который совсем не похож на настоящий веб-фреймворк, имеет очень мало смысла и только подрывает доверие ко всему тесту TechEmpower Framework Benchmark.
Возможно, я немного слишком критичен, но эта строка кода действительно заставила меня задуматься:
Установка HTTP-заголовка Date со значением даты и времени - это настолько небольшая задача, что для ее выполнения вам даже не понадобится фреймворк. Это должно быть не более одной строки кода:
response.WriteHeader("Date", DateTime.UtcNow.ToString())
Однако бенчмарк ASP.NET Core имеет "немного более оптимизированное" решение этой задачи:
Установка значения даты и времени была настолько сильно оптимизирована, что я даже не могу уместить весь код на одном экране. Творческий подход к поиску способов экономии вычислительных циклов и, соответственно, более высоких результатов в бенчмарках просто поражает. Класс DateHeader
- это статический класс (что означает, что он инициализируется только один раз как синглтон и затем хранится в памяти) со статическим значением DateTimeOffset
(конечно, уже сохраненным в виде массива байтов). Кроме того, объект System.Threading.Timer
также инициализируется статически с интервалом в одну секунду. Этот таймер будет работать в отдельном потоке и раз в секунду устанавливать новое значение времени даты:
private static readonly Timer s_timer = new Timer((s) => {
SetDateValues(DateTimeOffset.UtcNow);
}, null, 1000, 1000);
Вы задаетесь вопросом, как это можно оптимизировать? Ну, TechEmpower Benchmark будет обращаться к веб-серверу сотни тысяч раз в секунду, чтобы действительно проверить пределы возможностей каждого фреймворка. Класс DateHeader
будет возвращать одну и ту же временную метку для всех этих тысяч запросов и, таким образом, избавит себя от необходимости вычислять новую временную метку много тысяч раз. Затем через секунду Timer
(который работает в отдельном потоке) синхронизирует новую временную метку ровно один раз и кэширует ее для следующих 300+ тысяч запросов. Я поражен изобретательностью. По правде говоря, HTTP заголовок Date
не принимает временные метки с точностью более секунды, и в руководстве TechEmpower упоминается, что это принятая оптимизация.
Единственный вопрос, который у меня возникает: если этот бенчмарк тестирует ASP.NET Core, зачем ему нужно повторять то, что ASP.NET Core уже имеет из коробки?
Теперь я спрашиваю себя, все ли бенчмарки ASP.NET Core "подправлены" подобным образом?
А как насчет других фреймворков?
Мне нужно было продолжить расследование!
Бенчмарк "ASP.NET Core Micro"
После изучения "Platform" бенчмарков пришло время взглянуть на "Micro" фреймворки:
Если посмотреть на соответствующий Dockerfile, то окажется, что бенчмарки "Micro" используют код из папки /Benchmarks
, который выглядит как настоящее приложение ASP.NET Core:
Этот бенчмарк сразу же отличается от предыдущего. Мне очень приятно видеть, что в нем используются элементы, взятые из самого ASP.NET Core. Тесты Fortunes инициализируются с помощью обычных middleware следующим образом:
Бенчмарк aspcore-mw-ado-pg
- это то, что большинство разработчиков .NET, вероятно, назвали бы низкоуровневой "платформенной" реализацией ASP.NET Core. Здесь нет ни маршрутизации более высокого уровня, ни согласования содержимого, ни других cross-cutting middleware'ов, ни EntityFramework, ни фактического рендеринга шаблонов HTML, но, по крайней мере, это ASP.NET Core.
Middleware работает непосредственно с HttpContext
для выполнения базовой маршрутизации:
Это нормально и соответствует рекомендациям TechEmpower, поскольку работа непосредственно с HttpContext является канонической для фреймворка (в отличие от предыдущего бенчмарка):
В некоторых случаях считается нормальным и уместным для производственного уровня использование ручной минималистичной маршрутизации с использованием управляющих структур, таких как ветвление if/else. Это допустимо, если это считается каноническим для фреймворка.
Хотя бенчмарк middleware'а больше не применяет трюк с AsciiString
, он все еще прибегает к "фальшивому" шаблонизатору:
В целом, это гораздо более реалистичный (хоть и не идеальный) бенчмарк!
Бенчмарк "ASP.NET Core Full"
Наконец, пришло время проверить бенчмарк "MVC". Он также берет свой код из папки /Benchmarks
, но вместо работы с необработанным HttpContext
он фактически инициализирует минимально необходимое MVC middleware с Razor View Engine:
Поведение контроллера также выполнено очень реалистично и, наконец, использует реальный движок шаблонов ASP.NET Core:
[HttpGet("raw")]
public async Task<IActionResult> Raw()
{
var db = HttpContext.RequestServices.GetRequiredService<RawDb>();
return View("Fortunes", await db.LoadFortunesRows());
}
Razor view соответствует тому, что можно ожидать от этого простого бенчмарка:
Это наиболее реалистичное приложение ASP.NET Core, которое действительно соответствует духу бенчмарка Fortunes.
Однако результаты этого бенчмарка сильно отличаются от того, что Microsoft активно рекламировала сообществу .NET. Разница в производительности между "ненастоящим" шаблонизатором, где HTML-ответ создается в памяти через кэшированный StringBuilder, и реальным шаблонизатором, которому приходится выполнять дополнительные (дорогостоящие) операции ввода-вывода для чтения, разбора и применения HTML-шаблонов с диска, огромна.
Последнему удается обслуживать только 184 тыс. запросов/сек и он занимает лишь 109 место в общем рейтинге TechEmpower Framework Benchmarks для теста Fortunes. Это ошеломляющая разница, которую следует иметь в виду, сравнивая ASP.NET Core с фреймворками, написанными на Java, Go или C++.
Другие фреймворки
Теперь, когда я составил более четкое представление о различных показателях бенчмарков ASP.NET Core, пришло время посмотреть и на другие фреймворки.
Java
Самый быстрый Java-бенчмарк, который также использует Postgres в качестве исходной базы данных, - это Jooby.
Их реализация бенчмарка поразительно проста. Вся реализация Fortune представляет собой следующий блок кода:
Он использует маршрутизатор более высокого уровня (get("/fortunes", ctx -> {}
), а также обычные методы доступа к базе данных и настоящий шаблонизатор:
Это практически Java-эквивалент бенчмарка ASP.NET Core MVC (он же Full).
Интересно то, что этот совершенно неоптимизированный полноценный Java MVC фреймворк занимает 12 место в бенчмарке Fortunes с невероятными 404 тыс. запросов/сек. По сути, это более чем в два раза быстрее, чем эквивалент ASP.NET Core, и он все еще выигрывает у "Micro" реализации ASP.NET Core (которая пропускает все дорогостоящие операции ввода-вывода, используя поддельный движок шаблонов) и даже может конкурировать с печально известным приложением /PlatformBenchmarks
, которое, честно говоря, из-за его различий даже не стоит сравнивать.
Никакого неуважения к ASP.NET Core (потому что 184 тыс. запросов/сек - это все равно потрясающий результат), но он и близко не стоит с этим Java-фреймворком, когда речь идет о производительности. Отдадим должное.
Go
А что насчет Go?
Себастьен Рос (разработчик, работающий над производительностью ASP.NET Core в Microsoft) особо отметил Go и заявил, что ASP.NET Core все еще быстрее Go в сравнении "один на один". Меня лично очень заинтересовало это утверждение, поскольку я перевел несколько проектов .NET Core на Go и увидел значительный рост производительности в результате этого.
На момент написания этого сообщения самым быстрым бенчмарком Fortune является atreugo для Go.
Как и в Java, фактическая реализация Go предельно проста.
Маршрутизация осуществляется с помощью идиом, предоставляемых фреймворком:
Здесь не нужно искать коротких путей или хитростей. Все приложение для бенчмарка Fortunes состоит из менее чем 20 строк кода.
Шаблонизация также выполнена надлежащим образом:
Итак, что же получается в целом? Ну, как и в случае с быстрым фреймворком Java, бенчмарк Go также сравнивается с лучшей "полной" реализацией ASP.NET Core. Иное было бы просто несправедливо. Нельзя сравнивать бенчмарк, который выплевывает созданный в памяти HTML (который даже не является частью ASP.NET Core), с бенчмарком, который использует реальный движок шаблонов, который проходит через дорогостоящие циклы чтения файлов из ввода-вывода, их разбора во время выполнения и выполнения логики для каждого запроса (циклы, переменные и т.д. в шаблоне).
Тем не менее, дорогая реализация Go занимает 22 место в TechEmpower Fortunes Benchmark с не менее впечатляющими 381 тыс. запросов/сек. Не так быстро, как Java, но все же более чем в 2 раза быстрее, чем аналогичный тест в ASP.NET Core.
C++
Надеюсь, это не должно быть для вас большим сюрпризом, в настоящее время C++ с фреймворком drogon лидирует в бенчмарках Fortunes с умопомрачительной скоростью 616 тыс. запросов/сек, что с большим отрывом опережает все остальные фреймворки (кроме Rust, где разрыв не так велик)! Что делает это достижение еще более поразительным, так это то, что ему удалось сделать это с помощью полноценной реализации MVC. Здесь нет абсолютно никаких недомолвок или хитростей.
Он даже использует шаблонизатор CSP, который выглядит следующим образом:
Я люблю .NET, но нет такой умственной гимнастики, которую можно было бы убедительно применить, чтобы .NET оказался выше C++. Любой бенчмарк, который считает иначе, знает, что он не честен с самим собой.
Rust, Node.js, Kotlin and PHP
Поскольку команда .NET начала рекламировать ASP.NET Core как гораздо более быстрый веб-фреймворк, чем многие другие, я решил, что будет справедливо проверить эти утверждения.
Rust
Rust обеспечивает 588 тыс. запросов/сек и занимает 2-е место в общем бенчмарке Fortunes. Это единственная иная языковая платформа, которая ходит по пятам за C++. Фреймворк xitca-web достигает такого невероятного результата благодаря еще одной правильной MVC-подобной реализации и настоящему шаблонизатору.
Kotlin
Еще один отличный результат достигнут веб-фреймворком Kotlin с очень честной реализацией Fortunes, которая использует движок Rocker для шаблонизации HTML. Она достигает 350 тыс. запросов/сек и занимает 29-е место в общем зачете, что на 80 позиций опережает аналогичную реализацию ASP.NET Core.
Node.js
Одно из утверждений, которое оказалось (частично) верным, заключается в том, что ASP.NET Core быстрее Node.js. Хотя ASP.NET Core всего в 3 раза, а не в 10 раз быстрее, как было заявлено, он все же убедительно побеждает Polkadot, который является самым рейтинговым Node.js фреймворком, имеющим реализацию, сравнимую с бенчмарком "Micro" в ASP.NET Core. Со скоростью всего 125 тыс. запросов/сек Node.js он отстает от .NET.
PHP
Это может застать людей врасплох, но если вы не были внимательны, то вы могли пропустить всю работу, которая была проделана над PHP за многие годы. Не в последнюю очередь потому, что Facebook вложил много усилий в то, чтобы сделать PHP лучшей платформой. Теперь он способен обслуживать невероятные 309 тысяч запросов/сек с помощью MVC-подобной реализации, предоставляемой mixphp. Это все еще значительно быстрее, чем MVC-фреймворк ASP.NET Core, и, безусловно, заслуживает упоминания!
Just(js)
Если вы являетесь JavaScript разработчиком, не расстраивайтесь из-за бенчмарков Node.js, потому что Just(js) сбивает с ног впечатляющими 538 тыс. запросов/сек. Это не шутка, Just(js) занимает 5-е место в бенчмарке Fortunes и является единственным фреймворком, который конкурирует с C++ и Rust. Это выдающееся достижение, которое не является чем-то случайным. Он намного опережает все остальные бенчмарки ASP.NET Core и должен быть упомянут в этом посте!
Действительно ли ASP.NET Core быстрый?
Да, это точно так!
Особенно если вспомнить, чем был классический ASP.NET во времена .NET Framework, то становится ясно, что ASP.NET Core отличается от своего темного прошлого.
Не заблуждайтесь, ASP.NET Core очень быстр и, конечно, не должен уклоняться от здоровой конкуренции. Однако, очевидно, что он не быстрее Java, Go или C++. Возможно, когда-нибудь он достигнет этого, но на данный момент это не так. Я уверен, что мы еще не увидели потолка для ASP.NET Core, и я с нетерпением жду того, что команда .NET сделает дальше. ASP.NET Core - отличная платформа, и даже если она не самая быстрая (пока), она все равно приносит радость!
Хотелось бы, чтобы Скотт Хантер и остальные члены команды ASP.NET Core не чувствовали необходимости продвигать ASP.NET Core на рынке, основываясь на мягкой лжи и недобросовестных заявлениях, чтобы выделить ASP.NET Core среди аналогов. Я уверен, что здесь есть много чего другого, чем можно гордиться!
Автор:
Whishare