В экосистеме
Репозиторий с исходниками:
https://github.com/karenpayneoregon/csharp-basics-2025/blob/master/AspCoreHelperLibrary/WindowHelper.cs
👉 @KodBlog
ASP.NET Core появилась небольшая, но полезная утилита для управления консольным окном. Разработчик выложил на GitHub пример кода, который позволяет принудительно показать консоль для логирования и менять её заголовок по своему вкусу. Инструмент уже протестирован на Windows 11 и может пригодиться тем, кто пишет сервисы с дополнительным выводом в терминал.Репозиторий с исходниками:
https://github.com/karenpayneoregon/csharp-basics-2025/blob/master/AspCoreHelperLibrary/WindowHelper.cs
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1🤔1🥴1
В C# это довольно удобно: var завезли как раз тогда, когда добавили анонимные типы.
Хотя код тут чисто для демонстрации, забавно, что никто не заметил тупой баг на строке 4.
👉 @KodBlog
Хотя код тут чисто для демонстрации, забавно, что никто не заметил тупой баг на строке 4.
Please open Telegram to view this post
VIEW IN TELEGRAM
👎3❤1
Rider 2025.3 Release Candidate уже доступен!
В этой сборке собраны все изменения и улучшения, которые будут в следующем крупном релизе. Попробуй и поделись впечатлениями о новых фичах и производительности Rider ;)
Подробнее и ссылка на скачивание тут : https://blog.jetbrains.com/dotnet/2025/11/05/the-rider-2025-3-release-candidate/
👉 @KodBlog
В этой сборке собраны все изменения и улучшения, которые будут в следующем крупном релизе. Попробуй и поделись впечатлениями о новых фичах и производительности Rider ;)
Подробнее и ссылка на скачивание тут : https://blog.jetbrains.com/dotnet/2025/11/05/the-rider-2025-3-release-candidate/
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
Добавляем Описание в Параметризованные Тесты
Часто нам требуется протестировать несколько вариантов использования метода с разными данными, и для этого подойдут параметризованные тесты, например, Theory в xUnit.
При этом бывает полезно добавить не только тестовые данные, но и описание к каждому тестовому случаю. Рассмотрим, как это сделать.
Представим, что у нас есть такой тест:
Тест принимает следующую запись с тестовыми данными:
Если мы выполним тест, мы увидим в окне выполнения теста что-то вроде следующего:
Как видите, сложно понять, о чём каждый тестовый случай, особенно учитывая, что у нас есть коллекции в параметрах. Но обратите внимание, что из-за использования record, мы видим строковое представление записи, т.к. среда выполнения вызывает метод ToString() параметров. Мы можем использовать это.
Чтобы заставить среду выводить более осмысленное описание, мы можем добавить описание теста в LimitDesignerFilters и переопределить метод ToString():
Теперь мы можем задать свойству Description описание каждого тестового случая:
Тогда в окне выполнения теста мы увидим следующее:
Тут всё ещё присутствует название параметра (filters), но всё же, понять, что проверяет каждый тест, уже гораздо проще.
👉 @KodBlog
Часто нам требуется протестировать несколько вариантов использования метода с разными данными, и для этого подойдут параметризованные тесты, например, Theory в xUnit.
При этом бывает полезно добавить не только тестовые данные, но и описание к каждому тестовому случаю. Рассмотрим, как это сделать.
Представим, что у нас есть такой тест:
[Theory]
[MemberData(nameof(InvalidFilters))]
public async Task ShouldNotAllowInvalidInvariants(
LimitFilters filters)
{
…
}
Тест принимает следующую запись с тестовыми данными:
public record LimitFilters(
Guid? WorkpieceNumber,
IEnumerable<int>? Ids,
IEnumerable<int>? Tools,
IEnumerable<int>? LimitIds);
}
Если мы выполним тест, мы увидим в окне выполнения теста что-то вроде следующего:
ShouldNotAllowInvalidInvariants(filters: { WorkpieceNumber = … })
ShouldNotAllowInvalidInvariants(filters: { WorkpieceNumber = … })Как видите, сложно понять, о чём каждый тестовый случай, особенно учитывая, что у нас есть коллекции в параметрах. Но обратите внимание, что из-за использования record, мы видим строковое представление записи, т.к. среда выполнения вызывает метод ToString() параметров. Мы можем использовать это.
Чтобы заставить среду выводить более осмысленное описание, мы можем добавить описание теста в LimitDesignerFilters и переопределить метод ToString():
public record LimitDesignerFilters(
string Description,
Guid? WorkpieceNumber,
IEnumerable<int>? Ids,
IEnumerable<int>? Tools,
IEnumerable<int>? LimitIds)
{
public override string ToString()
=> Description;
}
Теперь мы можем задать свойству Description описание каждого тестового случая:
public static TheoryData<LimitDesignerFilters>
InvalidFilters =>
[
new("Workpiece is null", null, [1], [1], [1]),
new("Param1 is null", Guid.NewGuid(), null, [1], [1]),
];
Тогда в окне выполнения теста мы увидим следующее:
ShouldNotAllowInvalidInvariants(filters: Param1 is null)
ShouldNotAllowInvalidInvariants(filters: Workpiece is null)
Тут всё ещё присутствует название параметра (filters), но всё же, понять, что проверяет каждый тест, уже гораздо проще.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8
В EF Core 10 (preview) появилась поддержка нескольких глобальных фильтров на одну сущность
Раньше в Entity Framework Core можно было задать только один HasQueryFilter для каждой сущности, что было неудобно, если нужно, например, одновременно фильтровать по IsDeleted и TenantId. Теперь в версии 10 это можно делать, но главное, дать каждому фильтру имя.
Пример:
Теперь можно управлять каждым фильтром отдельно: включать, отключать или комбинировать как нужно.
Фича уже доступна в preview и обещает сильно упростить жизнь тем, кто работает с multi-tenant и soft delete сценариями.
👉 @KodBlog
Раньше в Entity Framework Core можно было задать только один HasQueryFilter для каждой сущности, что было неудобно, если нужно, например, одновременно фильтровать по IsDeleted и TenantId. Теперь в версии 10 это можно делать, но главное, дать каждому фильтру имя.
Пример:
modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
.HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);
Теперь можно управлять каждым фильтром отдельно: включать, отключать или комбинировать как нужно.
Фича уже доступна в preview и обещает сильно упростить жизнь тем, кто работает с multi-tenant и soft delete сценариями.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6🔥1
Небольшая деталь, но именно она делает работу с данными точной и предсказуемой
Не вызывай несколько OrderBy подряд. Каждый новый OrderBy пересортирует коллекцию заново и предыдущая сортировка пропадет. Для дополнительного критерия сортировки используй ThenBy.
Плохой вариант:
Правильный вариант:
👉 @KodBlog
Не вызывай несколько OrderBy подряд. Каждый новый OrderBy пересортирует коллекцию заново и предыдущая сортировка пропадет. Для дополнительного критерия сортировки используй ThenBy.
Плохой вариант:
products
.OrderBy(x => x.Name)
.OrderBy(x => x.Price) // всё пересортирует по цене
Правильный вариант:
products
.OrderBy(x => x.Name)
.ThenBy(x => x.Price)
Please open Telegram to view this post
VIEW IN TELEGRAM
❤17👍12
Скрипт PowerShell для переименования проектов .NET
Переименовать проект .NET - утомительное занятие. Вам придётся переименовать файлы и папки, а также заменить содержимое в файлах, например пространство имён или путь в файлах .sln.
Следующий скрипт PowerShell, переименует файлы и папки и заменит содержимое в файлах:
👉 @KodBlog
Переименовать проект .NET - утомительное занятие. Вам придётся переименовать файлы и папки, а также заменить содержимое в файлах, например пространство имён или путь в файлах .sln.
Следующий скрипт PowerShell, переименует файлы и папки и заменит содержимое в файлах:
$ErrorActionPreference = "Stop"
$rootFolder = Resolve-Path -Path "."
$oldName = "SampleRazorPages"
$newName = "SampleWebApp"
# Переименовываем файлы и папки
foreach ($item in Get-ChildItem -LiteralPath $rootFolder -Recurse | Sort-Object -Property FullName -Descending) {
$itemNewName = $item.Name.Replace($oldName, $newName)
if ($item.Name -ne $itemNewName) {
Rename-Item -LiteralPath $item.FullName -NewName $itemNewName
}
}
# Заменяем содержимое в файлах
foreach ($item in Get-ChildItem $rootFolder -Recurse -Include "*.cmd", "*.cs", "*.csproj", "*.json", "*.md", "*.proj", "*.props", "*.ps1", "*.sln", "*.slnx", "*.targets", "*.txt", "*.vb", "*.vbproj", "*.xaml", "*.xml", "*.xproj", "*.yml", "*.yaml") {
$content = Get-Content -LiteralPath $item.FullName
if ($content) {
$newContent = $content.Replace($oldName, $newName)
Set-Content -LiteralPath $item.FullName -Value $newContent
}
}
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🔥2
Насколько зрел формат dotnet .slnx?
Видел этот вопрос на Reddit. Насколько я понимаю, формат уже готов к использованию. Кто-нибудь его уже применяет?
Пример нового формата .slnx для dotnet → на фото
Это старый скриншот (сейчас уже доступен в стабильной версии Visual Studio), но видно, что новый формат стал заметно менее многословным.
👉 @KodBlog
Видел этот вопрос на Reddit. Насколько я понимаю, формат уже готов к использованию. Кто-нибудь его уже применяет?
Пример нового формата .slnx для dotnet → на фото
Это старый скриншот (сейчас уже доступен в стабильной версии Visual Studio), но видно, что новый формат стал заметно менее многословным.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7🤔5
Оптимизация roundtrip в Entity Framework 7
Хорошая оптимизация убирала лишние транзакции, если число вставляемых строк меньше или равно размеру батча 42 у провайдера SQL Server.
👉 @KodBlog
Хорошая оптимизация убирала лишние транзакции, если число вставляемых строк меньше или равно размеру батча 42 у провайдера SQL Server.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3😁3🤔1
This media is not supported in your browser
VIEW IN TELEGRAM
Если вы когда-нибудь работали с базами данных, то знаете про LEFT JOIN (и, соответственно, RIGHT JOIN). Это одна из самых частых операций в SQL.
Но в Entity Framework Core реализовать левое соединение всегда было немного мучением.
Проблема старого LINQ
До .NET 10 в LINQ не было прямого способа выразить LEFT JOIN или RIGHT JOIN.
Вместо этого приходилось городить конструкцию через GroupJoin + DefaultIfEmpty, чтобы сохранить строки из левой таблицы даже при отсутствии совпадений.
Это работало, но выглядело громоздко и плохо читалось.
Пример (старый способ)
Генерируемый SQL:
Метод-синтаксис был ещё менее читаем — GroupJoin, SelectMany, DefaultIfEmpty и прочие танцы с бубном.
Новое в .NET 10: LeftJoin и RightJoin
Теперь в LINQ появились встроенные методы LeftJoin() и RightJoin(), которые наконец-то делают ровно то, что от них ожидаешь.
EF Core корректно транслирует их в SQL-запрос с LEFT JOIN или RIGHT JOIN.
Пример LeftJoin
Результирующий SQL тот же, что и раньше, но код стал в разы короче и понятнее.
Теперь сразу видно намерение: LeftJoin - значит левое соединение.
RightJoin
RightJoin делает обратное: сохраняет все строки из правой таблицы, добавляя данные из левой, если они есть.
EF Core транслирует это в RIGHT JOIN.
SQL:
Несколько советов:
- В проекции защитите сторону, допускающую null:
- Сохраняйте проекции небольшими, чтобы не извлекать больше столбцов, чем необходимо;
- Добавляйте индексы по ключам объединений для улучшения планов запросов.
👉 @KodBlog
Но в Entity Framework Core реализовать левое соединение всегда было немного мучением.
Проблема старого LINQ
До .NET 10 в LINQ не было прямого способа выразить LEFT JOIN или RIGHT JOIN.
Вместо этого приходилось городить конструкцию через GroupJoin + DefaultIfEmpty, чтобы сохранить строки из левой таблицы даже при отсутствии совпадений.
Это работало, но выглядело громоздко и плохо читалось.
Пример (старый способ)
var query =
from product in dbContext.Products
join review in dbContext.Reviews on product.Id equals review.ProductId into reviewGroup
from subReview in reviewGroup.DefaultIfEmpty()
orderby product.Id, subReview.Id
select new
{
ProductId = product.Id,
product.Name,
product.Price,
ReviewId = (int?)subReview.Id ?? 0,
Rating = (int?)subReview.Rating ?? 0,
Comment = subReview.Comment ?? "N/A"
};
Генерируемый SQL:
SELECT
p."Id" AS "ProductId",
p."Name",
p."Price",
COALESCE(r."Id", 0) AS "ReviewId",
COALESCE(r."Rating", 0) AS "Rating",
COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Products" AS p
LEFT JOIN "Reviews" AS r ON p."Id" = r."ProductId"
ORDER BY p."Id", COALESCE(r."Id", 0)
Метод-синтаксис был ещё менее читаем — GroupJoin, SelectMany, DefaultIfEmpty и прочие танцы с бубном.
Новое в .NET 10: LeftJoin и RightJoin
Теперь в LINQ появились встроенные методы LeftJoin() и RightJoin(), которые наконец-то делают ровно то, что от них ожидаешь.
EF Core корректно транслирует их в SQL-запрос с LEFT JOIN или RIGHT JOIN.
Пример LeftJoin
var query = dbContext.Products
.LeftJoin(
dbContext.Reviews,
product => product.Id,
review => review.ProductId,
(product, review) => new
{
ProductId = product.Id,
product.Name,
product.Price,
ReviewId = (int?)review.Id ?? 0,
Rating = (int?)review.Rating ?? 0,
Comment = review.Comment ?? "N/A"
})
.OrderBy(x => x.ProductId)
.ThenBy(x => x.ReviewId);
Результирующий SQL тот же, что и раньше, но код стал в разы короче и понятнее.
Теперь сразу видно намерение: LeftJoin - значит левое соединение.
RightJoin
RightJoin делает обратное: сохраняет все строки из правой таблицы, добавляя данные из левой, если они есть.
EF Core транслирует это в RIGHT JOIN.
var query = dbContext.Reviews
.RightJoin(
dbContext.Products,
review => review.ProductId,
product => product.Id,
(review, product) => new
{
ProductId = product.Id,
product.Name,
product.Price,
ReviewId = (int?)review.Id ?? 0,
Rating = (int?)review.Rating ?? 0,
Comment = review.Comment ?? "N/A"
});
SQL:
SELECT
p."Id" AS "ProductId",
p."Name",
p."Price",
COALESCE(r."Id", 0) AS "ReviewId",
COALESCE(r."Rating", 0) AS "Rating",
COALESCE(r."Comment", 'N/A') AS "Comment"
FROM "Reviews" AS r
RIGHT JOIN "Products" AS p ON r."ProductId" = p."Id"
Несколько советов:
- В проекции защитите сторону, допускающую null:
review.Comment ?? "N/A";- Сохраняйте проекции небольшими, чтобы не извлекать больше столбцов, чем необходимо;
- Добавляйте индексы по ключам объединений для улучшения планов запросов.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9🔥6
Краткое обновление по .NET 10 — версия готовится к выходу и приносит много интересного. Ниже главное, что удалось собрать.
✅ Подтверждённые изменения:
Версия .NET 10 получит статус LTS (Long-Term Support) с поддержкой несколько лет.
Основные улучшения в рантайме: оптимизация JIT, улучшенная инлайнинг/девиртуализация, улучшения для struct-аргументов.
Новое в C# 14: поддержка модификаторов параметров в лямбдах (ref, in, out), расширенные возможности field-ключевого слова, «extension blocks», null-conditional assignment и др.
В ASP.NET Core / Blazor: улучшения производительности, новые возможности для WebAssembly, улучшенная валидация форм и др.
Несколько оговорок:
Хотя вы могли увидеть сообщение «.NET 10 уже вышел сегодня», официальная GA-дата запланирована на 11 ноября 2025.
InfoQ
Сейчас доступны версии RC (Release Candidate) с поддержкой «go-live».
Не все анонсированные функции ещё подтверждены полностью либо ещё не вошли в стабильную версию.
👉 @KodBlog
Версия .NET 10 получит статус LTS (Long-Term Support) с поддержкой несколько лет.
Основные улучшения в рантайме: оптимизация JIT, улучшенная инлайнинг/девиртуализация, улучшения для struct-аргументов.
Новое в C# 14: поддержка модификаторов параметров в лямбдах (ref, in, out), расширенные возможности field-ключевого слова, «extension blocks», null-conditional assignment и др.
В ASP.NET Core / Blazor: улучшения производительности, новые возможности для WebAssembly, улучшенная валидация форм и др.
Несколько оговорок:
Хотя вы могли увидеть сообщение «.NET 10 уже вышел сегодня», официальная GA-дата запланирована на 11 ноября 2025.
InfoQ
Сейчас доступны версии RC (Release Candidate) с поддержкой «go-live».
Не все анонсированные функции ещё подтверждены полностью либо ещё не вошли в стабильную версию.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5🔥5👍4
This media is not supported in your browser
VIEW IN TELEGRAM
Понимай любую кодовую базу за секунды прямо в VS Code 🤯
Это расширение превращает весь твой проект в наглядную архитектурную визуализацию / показывает, как всё связано, двигается и зависит друг от друга.
Идеально, когда ты подключаешься к новому репозиторию или возвращаешься к старому.
Что делать:
1. Установи расширение Swark
2. Открой Command Palette (Ctrl + Shift + P)
3. Введи “Swark: Create Architecture Diagram” и выбери команду
4. Укажи корневую папку проекта
👉 @KodBlog
Это расширение превращает весь твой проект в наглядную архитектурную визуализацию / показывает, как всё связано, двигается и зависит друг от друга.
Идеально, когда ты подключаешься к новому репозиторию или возвращаешься к старому.
Что делать:
1. Установи расширение Swark
2. Открой Command Palette (Ctrl + Shift + P)
3. Введи “Swark: Create Architecture Diagram” и выбери команду
4. Укажи корневую папку проекта
Please open Telegram to view this post
VIEW IN TELEGRAM
❤14🔥5👎1
.NET 10 уже вышел!
Вот главные обновления по ключевым направлениям:
Нужна стартовая точка под .NET 10? Пора обновляться.
👉 @KodBlog
Вот главные обновления по ключевым направлениям:
C# 14
— расширяемые члены, свойства с полями, implicit spans и обновлённый nameof с поддержкой лямбд. Код стал чище и лаконичнее.
ASP.NET Core
— улучшенная поддержка OpenAPI, встроенная валидация для Minimal API, Server-Sent Events (SSE) и аутентификация с passkey.
EF Core
— добавлен поиск по векторам в SQL, улучшен LINQ–SQL перевод, появились Complex Types и полнотекстовый поиск в Cosmos DB.
Runtime
— прокачанный JIT-компилятор, больше stack allocation, поддержка AVX10.2 и улучшенный NativeAOT — приложения стали легче и быстрее.
Библиотеки
— новые криптографические API, свежие опции для JSON-сериализации, новый WebSocketStream API и ускоренная работа с ZipArchive.
SDK
— апдейты для file-based apps, поддержка контейнеров в консольных приложениях, tab-completion и команда dotnet tool exec.
Aspire
— полноценная поддержка Python и JS, деплой через Aspire Do, контейнеры как артефакты и новый AppHost CLI.
.NET MAUI
— улучшена диагностика и телеметрия верстки, добавлен XAML source generator, обновлён MediaPicker и шаблон Aspire service-defaults.
Нужна стартовая точка под .NET 10? Пора обновляться.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤18👍9👏2🥴1
Похоже, Entity Framework Core с .SqlQuery становится моим новым основным способом доступа к данным.
По моим неофициальным замерам, производительность выше, чем у Dapper, и при этом всё гораздо проще.
Чуть-чуть медленнее, чем чистый
DbContext при этом остаётся максимально лёгким.
👉 @KodBlog
По моим неофициальным замерам, производительность выше, чем у Dapper, и при этом всё гораздо проще.
Чуть-чуть медленнее, чем чистый
ADO.NET, но разница минимальная.DbContext при этом остаётся максимально лёгким.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18👍8❤7
Extension members в C# 14, пожалуй, лучшее, что Microsoft добавила в язык. Теперь можно писать такой чистый и лаконичный код.
👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🥴29👍9🔥4🤔3👏1
Новая версия AsyncAwaitBestPractices v10.0.0
✅ Добавлена поддержка .NET 10
Та самая библиотека async/await, которую все любят, теперь собрана и оптимизирована под .NET 10.
👉 @KodBlog
Та самая библиотека async/await, которую все любят, теперь собрана и оптимизирована под .NET 10.
Please open Telegram to view this post
VIEW IN TELEGRAM
GitHub
GitHub - TheCodeTraveler/AsyncAwaitBestPractices: Extensions for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask
Extensions for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask - TheCodeTraveler/AsyncAwaitBestPractices
🥴7👍4🔥2
Оптимизация запроса в EF Core снизила время выполнения с 30 секунд до 30 миллисекунд.
Вот какие шаги помогли
Исходный запрос был из реального проекта соцсети.
Сущности и связи:
Users — у каждого много постов и комментариев
Comments — привязаны к юзеру и посту
Categories — у постов есть категория
Posts — имеют категорию и множество лайков
Likes — относятся к конкретному посту
Задача:
Выбрать топ 5 пользователей, которые оставили больше всего комментариев за последние 7 дней под постами категории .NET.
Для каждого нужно вернуть:
- UserId
- Username
- количество комментариев (только под .NET постами за последние 7 дней)
- топ 3 .NET поста по лайкам, которые этот пользователь чаще всего комментировал (PostId, LikesCount)
Что было сделано для ускорения:
- Предфильтрация пользователей
- Ограничение до топ-5
- Сокращение числа JOIN
- Проекция только нужных полей
- Формирование результата в одном запросе
- Использование AsSplitQuery
- Трехфазный подход
- Двухфазный подход
Подробности можно посмотреть здесь
👉 @KodBlog
Вот какие шаги помогли
Исходный запрос был из реального проекта соцсети.
Сущности и связи:
Users — у каждого много постов и комментариев
Comments — привязаны к юзеру и посту
Categories — у постов есть категория
Posts — имеют категорию и множество лайков
Likes — относятся к конкретному посту
Задача:
Выбрать топ 5 пользователей, которые оставили больше всего комментариев за последние 7 дней под постами категории .NET.
Для каждого нужно вернуть:
- UserId
- Username
- количество комментариев (только под .NET постами за последние 7 дней)
- топ 3 .NET поста по лайкам, которые этот пользователь чаще всего комментировал (PostId, LikesCount)
Что было сделано для ускорения:
- Предфильтрация пользователей
- Ограничение до топ-5
- Сокращение числа JOIN
- Проекция только нужных полей
- Формирование результата в одном запросе
- Использование AsSplitQuery
- Трехфазный подход
- Двухфазный подход
Подробности можно посмотреть здесь
Please open Telegram to view this post
VIEW IN TELEGRAM
🥴10❤4👍2😁1
Media is too big
VIEW IN TELEGRAM
WinForms приложения на .NET Framework можно переехать в web на .NET 9+ и задеплоить в Azure App Services через Visual Studio и copilot за пару минут.
Вся логика приложения и бизнес-код сохраняются.
Сочетание Visual Studio 2026 и GitHub Copilot творит вещи.
👉 @KodBlog
Вся логика приложения и бизнес-код сохраняются.
Сочетание Visual Studio 2026 и GitHub Copilot творит вещи.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯15🥴4🤔3👍2