Совет по C#:
❌ if (x == null) { x = new List<string>(); }
✅ x ??= new List<string>();
Результат тот же, но в одну строку.
??= присваивает значение только если слева null.
Доступно начиная с C# 8.0.
Если вы предпочитате первый подход, это не значит что вы плохой разработчик😜
👉 @KodBlog
Результат тот же, но в одну строку.
??= присваивает значение только если слева null.
Доступно начиная с C# 8.0.
Если вы предпочитате первый подход, это не значит что вы плохой разработчик
Please open Telegram to view this post
VIEW IN TELEGRAM
❤21👍12😁4🥴2
Группировка данных
ToLookup это LINQ-метод, который группирует элементы по ключу. Похож на GroupBy, но есть пару нюансов, из-за которых он прям удобен в некоторых кейсах.
Он строит ILookup<TKey, TElement> это по сути готовая, неизменяемая коллекция групп. В каждой группе лежат элементы с одинаковым ключом.
Достаёшь группу через индексатор
ToLookup vs GroupBy:
GroupBy возвращает
ToLookup делает всё сразу и держит результат в памяти:
➡️ сгруппировал один раз при вызове
➡️ потом дергаешь группы сколько угодно, ничего не пересчитывается
➡️ но память под всю структуру выделяется сразу
Когда ToLookup заходит лучше всего
Используй, если нужно:
➡️ много раз обращаться к одним и тем же группам
➡️ не ловить лишние пересчёты группировки
➡️ получать пустую выборку вместо исключений при неизвестном ключе
➡️ иметь неизменяемую структуру
Не лучший выбор, если:
➡️ данных очень много и жалко память
➡️ нужна ленивость и потоковая обработка
➡️ группировка нужна один раз, тогда проще и дешевле GroupBy
👉 @KodBlog
ToLookup это LINQ-метод, который группирует элементы по ключу. Похож на GroupBy, но есть пару нюансов, из-за которых он прям удобен в некоторых кейсах.
Он строит ILookup<TKey, TElement> это по сути готовая, неизменяемая коллекция групп. В каждой группе лежат элементы с одинаковым ключом.
var users = new[]
{
new { Name = "Anna", Department = "Dev" },
new { Name = "Boris", Department = "Dev" },
new { Name = "Clara", Department = "QA" }
};
var lookup = users.ToLookup(u => u.Department);
foreach (var user in lookup["Dev"])
{
Console.WriteLine(user.Name); // Anna, Boris
}
Достаёшь группу через индексатор
lookup["Dev"]. Если такого ключа нет, вернётся пустая последовательность (без исключения).ToLookup vs GroupBy:
GroupBy возвращает
IEnumerable<IGrouping<TKey, TElement>> и работает с отложенным выполнением. То есть группировка реально происходит при переборе, и при повторном переборе может считаться заново.ToLookup делает всё сразу и держит результат в памяти:
Когда ToLookup заходит лучше всего
Используй, если нужно:
Не лучший выбор, если:
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍6
Забавный трюк для .NET: как в рантайме узнать, в каком файле и на какой строке объявлен метод
Почему не подходят
Потому что они работают только на месте вызова и требуют менять сигнатуры методов. А если тебе нужно взять локацию *любого*
Идея: Используем Portable PDB: в PDB лежит маппинг IL → исходники (файл, строки, колонки) через sequence points.
Настройка
Обычно portable PDB генерятся по умолчанию, но можно явно:
▪️ отдельным файлом:
▪️ или вшить в сборку:
Реализация (суть)
Читаем PE, ищем embedded pdb, если нет — пытаемся открыть
Минимальный скелет:
Пример использования:
Важный нюанс: в проде PDB могут быть не задеплоены специально (безопасность/размер/политики), так что всегда держим
Источник
@KodBlog
Почему не подходят
[CallerMemberName]/[CallerFilePath]/[CallerLineNumber]?Потому что они работают только на месте вызова и требуют менять сигнатуры методов. А если тебе нужно взять локацию *любого*
MethodInfo (например, для снапшот-тестов или логов) — это уже мимо. Идея: Используем Portable PDB: в PDB лежит маппинг IL → исходники (файл, строки, колонки) через sequence points.
Настройка
Обычно portable PDB генерятся по умолчанию, но можно явно:
<PropertyGroup>
<DebugType>portable</DebugType>
</PropertyGroup>
<PropertyGroup>
<DebugType>embedded</DebugType>
</PropertyGroup>
Реализация (суть)
Читаем PE, ищем embedded pdb, если нет — пытаемся открыть
.pdb рядом. Потом по MetadataToken метода достаем MethodDebugInformation, берем первый sequence point и вытаскиваем Document.Name (путь к файлу) + строку/колонку.Минимальный скелет:
// NuGet: System.Reflection.Metadata (9.*)
public static (string FilePath, SequencePoint SequencePoint)? GetMethodLocation(this MethodInfo methodInfo)
{
var location = methodInfo.DeclaringType?.Assembly.Location;
if (string.IsNullOrEmpty(location)) return null;
using var fs = File.OpenRead(location);
using var reader = new PEReader(fs);
var pdbProvider = reader.ReadDebugDirectory()
.Where(e => e.Type == DebugDirectoryEntryType.EmbeddedPortablePdb)
.Select(e => reader.ReadEmbeddedPortablePdbDebugDirectoryData(e))
.FirstOrDefault();
try
{
if (pdbProvider is null &&
!reader.TryOpenAssociatedPortablePdb(location, File.OpenRead, out pdbProvider, out _))
return null;
var pdbReader = pdbProvider!.GetMetadataReader();
var methodHandle = MetadataTokens.MethodDefinitionHandle(methodInfo.MetadataToken);
var mdi = pdbReader.GetMethodDebugInformation(methodHandle);
if (mdi.SequencePointsBlob.IsNil) return null;
var sp = mdi.GetSequencePoints().FirstOrDefault();
if (sp.Document.IsNil) return null;
var doc = pdbReader.GetDocument(sp.Document);
var filePath = pdbReader.GetString(doc.Name);
return (filePath, sp);
}
finally
{
pdbProvider?.Dispose();
}
}
Пример использования:
var method = typeof(Program).GetMethod(nameof(SampleMethod))!;
var loc = method.GetMethodLocation();
Console.WriteLine($"{loc?.FilePath}:{loc?.SequencePoint.StartLine}");
Важный нюанс: в проде PDB могут быть не задеплоены специально (безопасность/размер/политики), так что всегда держим
null-кейсы и не рассчитываем на 100% наличие. Источник
@KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤1
Расширяющий метод Entity Framework вместо TagWith.
👉 @KodBlog
public static class QueryableExtensions
{
public static IQueryable<T> TagWithDebugInfo<T>(
this IQueryable<T> query,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
var debugInfo = $"Caller: {memberName}, File: {filePath}, Line: {lineNumber}";
// также можно добавить другую информацию, которую вы хотите, например, userId и т. д.
return query.TagWith(debugInfo); //delegate to built in TagWith
}
}
//usage sample
var query = context.YourDbSet
.Where(x => x.SomeProperty == someValue)
.TagWithDebugInfo();
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8👏3🔥2
Пожалуйста, пересмотрите решение и всё-таки разрешите асинхронную валидацию моделей 👀
Сейчас это висит в майлстоуне “.NET 11 Planning”, очень хотелось бы, чтобы фичу реально довели до реализации.
@KodBlog
Сейчас это висит в майлстоуне “.NET 11 Planning”, очень хотелось бы, чтобы фичу реально довели до реализации.
@KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8💯5😐4❤🔥2
This media is not supported in your browser
VIEW IN TELEGRAM
Проектировать схемы БД становится сильно проще
Это бесплатный дизайнер схем, который работает на 100% локально, даже офлайн. Можно собирать как простые, так и сложные схемы, и ничего не улетает в облако.
Поддерживает импорт существующих схем, визуальное проектирование с нуля и экспорт результата в разные форматы и даже в код.
https://relatedb.com/
👉 @KodBlog
Это бесплатный дизайнер схем, который работает на 100% локально, даже офлайн. Можно собирать как простые, так и сложные схемы, и ничего не улетает в облако.
Поддерживает импорт существующих схем, визуальное проектирование с нуля и экспорт результата в разные форматы и даже в код.
https://relatedb.com/
Please open Telegram to view this post
VIEW IN TELEGRAM
❤18🔥3
OpenTelemetry + .NET: трейсы, метрики и логи в SigNoz
В статье показано развёртывание через docker-compose, генерация быстрых/медленных/ошибочных запросов и просмотр коррелированных трейсов, метрик и логов в SigNoz. Описаны настройки .NET (ResourceBuilder, ActivitySource, авто‑инструментирование, OTLP) и практические советы; исходники в репо.
👉 @KodBlog
В статье показано развёртывание через docker-compose, генерация быстрых/медленных/ошибочных запросов и просмотр коррелированных трейсов, метрик и логов в SigNoz. Описаны настройки .NET (ResourceBuilder, ActivitySource, авто‑инструментирование, OTLP) и практические советы; исходники в репо.
Please open Telegram to view this post
VIEW IN TELEGRAM
Хабр
Наблюдаемость .NET-сервисов с помощью OpenTelemetry (traces/metrics/logs). Практический пример
Привет, Хабр! TL;DR: Поднимаем стенд в docker-compose (gateway + api + postgres + otel-collector + SigNoz/ClickHouse). Делаем 3 запроса: быстрый / медленный / с исключением. Смотрим в SigNoz трейсы...
Хватит дергать DateTime.UtcNow прямо в бизнес-логике.
Строчка выглядит безобидно, но именно из-за нее тесты потом превращаются в кошмар
Вот типичная ловушка:
* DateTime.UtcNow > user.SubscriptionEnd
Вроде ок, да?
Но как только пытаешься это протестировать, вылезает проблема:
- ты не контролируешь DateTime.UtcNow
- тесты становятся флаки или обрастают костылями
- бизнес-правила оказываются привязаны к инфраструктуре
Есть варианты лучше:
1. Передавать DateTime параметром. Самый простой путь, отлично заходит для маленьких методов.
2. Вынести время в интерфейс IClock. Масштабируется лучше и дружит с тестами.
Когда ты инвертируешь зависимость от времени, доменная логика становится:
✅ проще тестировать
✅ более детерминированной
✅ с нормальным разделением ответственности
В следующий раз, когда пишешь условие, завязанное на время, остановись и спроси:
“Мне нужно будет это тестировать?”
Если да, выноси часы наружу.
@KodBlog
Строчка выглядит безобидно, но именно из-за нее тесты потом превращаются в кошмар
Вот типичная ловушка:
* DateTime.UtcNow > user.SubscriptionEnd
Вроде ок, да?
Но как только пытаешься это протестировать, вылезает проблема:
- ты не контролируешь DateTime.UtcNow
- тесты становятся флаки или обрастают костылями
- бизнес-правила оказываются привязаны к инфраструктуре
Есть варианты лучше:
1. Передавать DateTime параметром. Самый простой путь, отлично заходит для маленьких методов.
2. Вынести время в интерфейс IClock. Масштабируется лучше и дружит с тестами.
Когда ты инвертируешь зависимость от времени, доменная логика становится:
В следующий раз, когда пишешь условие, завязанное на время, остановись и спроси:
“Мне нужно будет это тестировать?”
Если да, выноси часы наружу.
@KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🥴9🔥3❤1👍1
Не пиши в SQL = NULL или != NULL
Для NULL всегда используй IS NULL / IS NOT NULL
NULL это “нет значения / неизвестно”, и обычные операторы сравнения (=, !=, <, >) с ним не работают, потому что NULL не равен, не больше и не меньше вообще ничего.
= / != сравнивают значения. А NULL это не значение, а отсутствие значения. Поэтому IS / IS NOT проверяют именно наличие или отсутствие значения.
👉 @KodBlog
Для NULL всегда используй IS NULL / IS NOT NULL
NULL это “нет значения / неизвестно”, и обычные операторы сравнения (=, !=, <, >) с ним не работают, потому что NULL не равен, не больше и не меньше вообще ничего.
= / != сравнивают значения. А NULL это не значение, а отсутствие значения. Поэтому IS / IS NOT проверяют именно наличие или отсутствие значения.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍23🔥3🥴3
Лучший конфиг для HttpClient это тот, который ты не копируешь в каждом сервисе.
В .NET ты настраиваешь все один раз через IHttpClientFactory (и Aspire, если используешь):
* service discovery для вызовов сервис-сервис
* дефолтную resiliency-настройку, чтобы кратковременные фейлы не ломали тебе жизнь
Дальше просто инжектишь клиент и пишешь бизнес-код.
- полный разбор
👉 @KodBlog
В .NET ты настраиваешь все один раз через IHttpClientFactory (и Aspire, если используешь):
* service discovery для вызовов сервис-сервис
* дефолтную resiliency-настройку, чтобы кратковременные фейлы не ломали тебе жизнь
Дальше просто инжектишь клиент и пишешь бизнес-код.
- полный разбор
Please open Telegram to view this post
VIEW IN TELEGRAM
👏6
25 Claude Code skills для .NET-разрабов
Покрывает штуки, которые реально важны и неочевидны (типа: как тестировать транзакционные письма в ASP.NET Core?) и вообще помогает улучшать вывод LLM (типы!).
github.com/Aaronontheweb/dotnet-skills?tab=readme-ov-file
Все это про то, чтобы Claude делал вещи нормально без того, чтобы ты на него орал.
👉 @KodBlog
Покрывает штуки, которые реально важны и неочевидны (типа: как тестировать транзакционные письма в ASP.NET Core?) и вообще помогает улучшать вывод LLM (типы!).
github.com/Aaronontheweb/dotnet-skills?tab=readme-ov-file
Все это про то, чтобы Claude делал вещи нормально без того, чтобы ты на него орал.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍2🥴2
БОЛЬШОЙ SQL грех
Никогда не пихай ORDER BY внутрь CTE (или подзапроса), если там же рядом нет LIMIT.
Если LIMIT забыть, база обычно будет сортировать всю таблицу вообще без смысла.
👉 @KodBlog
Никогда не пихай ORDER BY внутрь CTE (или подзапроса), если там же рядом нет LIMIT.
Если LIMIT забыть, база обычно будет сортировать всю таблицу вообще без смысла.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4❤3👏2👎1
Как пользоваться LINQ
Чтобы использовать LINQ, нужно подключить
➡️ Фильтрация через Where
Если использовать
Результат вывода ниже будет
➡️ Цикл через ForEach
Если использовать
Результат вывода ниже будет
➡️ Комбинации
LINQ при правильной комбинации может сильно сокращать код.
В примере ниже из
➡️ Пошаговая обработка через Select
В коде ниже каждый элемент массива строк по одному конвертируется в
(В других языках это что-то вроде
➡️ Any как OR-условие
В коде ниже проверяем, есть ли в списке имён хотя бы одно, которое начинается с
➡️ GetRange: взять диапазон элементов
В примере ниже при
➡️ Reverse: развернуть список
В коде ниже изначально список
➡️ Single: получить единственный элемент
В коде ниже пытаемся через
Для
➡️ Contains: проверить наличие элемента
В коде ниже проверяем, есть ли в списке
👉 @KodBlog
Чтобы использовать LINQ, нужно подключить
System.Linq через using.Если использовать
Where, можно коротко вытащить из массива/списка только элементы, которые подходят под условие.Результат вывода ниже будет
1.// список int
List<int> idList = new List<int>{1, 2, 3};
// фильтруем: берём только те, где 1
IEnumerable<int> selectList = idList.Where(id => id == 1);
Если использовать
ForEach, можно так же, как в обычном ForEach, сделать цикл короче, чем for.Результат вывода ниже будет
1,2,3.// список int
List<int> idList = new List<int>{1, 2, 3};
// проходимся по всему списку
idList.ForEach(id =>
{
print(id);
}
LINQ при правильной комбинации может сильно сокращать код.
В примере ниже из
attackList берутся элементы, где totalAttack != 0, затем они группируются по totalAttack, и группы сортируются по totalAttack.// группировка по totalAttack (одинаковый totalAttack, разные GridInfo)
IEnumerable<IGrouping<int, GridInfo>> total = attackList
.Where(info => info.totalAttack != 0) // фильтруем где есть totalAttack
.GroupBy(info => info.totalAttack) // группируем по totalAttack
.OrderBy(group => group.Key); // сортируем по totalAttack
Select позволяет легко обработать элементы массива по порядку.В коде ниже каждый элемент массива строк по одному конвертируется в
int, а затем весь результат превращается в List<int>.(В других языках это что-то вроде
Map: берём элементы массива и преобразуем)// массив String
string[] idString = {"1", "2", "3"};
// делаем int и превращаем в List
List<int> idList = idString.Select(id => int.Parse(id)).ToList();
Any возвращает true, если хотя бы один элемент удовлетворяет условию.В коде ниже проверяем, есть ли в списке имён хотя бы одно, которое начинается с
"A".// List строк
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
// Alice подходит, значит True
names.Any(name => name.StartsWith("A"));
GetRange позволяет взять из списка элементы по указанному диапазону.В примере ниже при
0〜5 можно получить "1, 2, 3, 4", но если указать диапазон за пределами размера списка, будет ошибка.// List int
List<int> idList = new List<int> { 1, 2, 3, 4, 5 };
// берём элементы 0〜4
idList.GetRange(0, 5);
// ошибка 1
idList.GetRange(0, 6);
// ошибка 2
idList.GetRange(1, 5);
Reverse разворачивает порядок элементов в списке.В коде ниже изначально список
"1, 2, 3, 4, 5", после Reverse станет "5, 4, 3, 2, 1".// List int
List<int> idList = new List<int> { 1, 2, 3, 4, 5 };
// выводим 1〜5
idList.ForEach(id =>
{
print(id);
});
// разворот
idList.Reverse();
// выводим 5〜1
idList.ForEach(id =>
{
print(id);
});
Single позволяет получить элемент, который встречается ровно один раз.В коде ниже пытаемся через
Where получить из списка "1, 1, 1, 1, 2" значения 2 и 1.Для
2 всё ок (он один), а для 1 будет ошибка, потому что 1 встречается несколько раз.// List int
List<int> idList = new List<int> { 1, 1, 1, 1, 2 };
// берём только 2
int select = idList.Where(id => 2).SingleOrDefault();
// ошибка
int select = idList.Where(id => 1).SingleOrDefault();
Contains позволяет проверить, есть ли элемент в списке.В коде ниже проверяем, есть ли в списке
"Alice, Bob, Charlie" значение "Alice".// List строк
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
// Alice есть в списке, значит True
names.Contains(name => name.StartsWith("Alice"));
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17🥰3
Фанфакт: в C# можно руками создать окно через Win32 API, прям как в C++.
По ощущениям это реально то же самое, что и в C++: регаешь window class, делаешь
Что в итоге получаешь: очень лёгкую штуку без лишнего оверхеда. Если использовать
И да, туда же спокойно добавляются кнопки и прочие контролы через тот же Win32. Выглядит олдскульно, но зато железобетонно понимаешь, как GUI под капотом работает.
👉 @KodBlog
По ощущениям это реально то же самое, что и в C++: регаешь window class, делаешь
CreateWindowEx, заводишь WndProc, гоняешь GetMessage/DispatchMessage. Практической пользы почти ноль по сравнению с WinForms, но поиграться прикольно :DЧто в итоге получаешь: очень лёгкую штуку без лишнего оверхеда. Если использовать
LibraryImport + NativeAOT (это быстрее и приятнее, чем классический P/Invoke), можно собрать примерно 1MB exe без DLL, который моментально стартует и открывает окно. Делать так в проде обычно незачем, это скорее чисто эксперимент.И да, туда же спокойно добавляются кнопки и прочие контролы через тот же Win32. Выглядит олдскульно, но зато железобетонно понимаешь, как GUI под капотом работает.
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔8👍4❤3👎1🤯1
Please open Telegram to view this post
VIEW IN TELEGRAM
😐32🥴5😁2🤯1🤨1
Два файла, которые я создаю еще до того, как напишу первую строку .NET-кода:
Directory.Packages.props и Directory.Build.props
Что это вообще такое?
Это файлы, которые ты добавляешь в solution, чтобы повысить сопровождаемость.
1/ Directory.Build.props
Этот файл централизует общие настройки проектов по всей solution.
✅ версия .NET
✅ nullable reference types
✅ предупреждения компилятора
✅ анализаторы
Вместо того чтобы копировать одну и ту же конфигурацию в каждый csproj, ты задаешь ее один раз.
Все проекты следуют одним правилам, поэтому со временем настройки не начинают “разъезжаться”.
2/ Directory.Packages.props
Этот файл управляет версиями NuGet-пакетов для всей solution.
Ты задаешь версии NuGet в одном месте, и все проекты используют именно их. В больших кодовых базах это убирает целый класс болезненных сюрпризов при сборках и деплоях.
Эти два файла особенно полезны при апгрейде версии .NET:
1. Меняешь в Directory.Build.props версию .NET.
2. Обновляешь версии пакетов в Directory.Packages.props.
3. Всё, готово.
Хочешь, чтобы .NET-решения нормально жили годами, начни с добавления этих двух простых файлов.
👉 @KodBlog
Directory.Packages.props и Directory.Build.props
Что это вообще такое?
Это файлы, которые ты добавляешь в solution, чтобы повысить сопровождаемость.
1/ Directory.Build.props
Этот файл централизует общие настройки проектов по всей solution.
Вместо того чтобы копировать одну и ту же конфигурацию в каждый csproj, ты задаешь ее один раз.
Все проекты следуют одним правилам, поэтому со временем настройки не начинают “разъезжаться”.
2/ Directory.Packages.props
Этот файл управляет версиями NuGet-пакетов для всей solution.
Ты задаешь версии NuGet в одном месте, и все проекты используют именно их. В больших кодовых базах это убирает целый класс болезненных сюрпризов при сборках и деплоях.
Эти два файла особенно полезны при апгрейде версии .NET:
1. Меняешь в Directory.Build.props версию .NET.
2. Обновляешь версии пакетов в Directory.Packages.props.
3. Всё, готово.
Хочешь, чтобы .NET-решения нормально жили годами, начни с добавления этих двух простых файлов.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤30👍5🍌2
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👌4
Все еще сидишь на .sln в 2026?
Давай покажу, с чем ты реально имеешь дело:
🤮 Везде GUID’ы, вечные merge-конфликты, читать невозможно.
А теперь то же самое в .slnx:
Вот и всё. Читаемо человеком, дружит с git, никаких GUID’ов.
Почему стоит мигрировать сейчас:
▪️ Поддерживается в Visual Studio 2022 (17.10+) и .NET 9+
▪️
▪️ Меньше merge-конфликтов, меньше боли в команде
▪️ В code review это наконец можно нормально читать
Как мигрировать:
Готово.
Старый формат делали под внутренности Visual Studio, не под людей. SLNX делают для разработчиков.
Все еще держишься за .sln? Почему?
👉 @KodBlog
Давай покажу, с чем ты реально имеешь дело:
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyApp", "MyApp\MyApp.csproj", "{8A3B4E29-7F7D-4C51-9A18-5D8B9A3E1234}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
.... +200 more unreadable lines?А теперь то же самое в .slnx:
<Solution>
<Project Path="MyApp/MyApp.csproj" />
</Solution>
Вот и всё. Читаемо человеком, дружит с git, никаких GUID’ов.
Почему стоит мигрировать сейчас:
dotnet sln migrate делает это одной командойКак мигрировать:
dotnet sln migrate MyApp.sln
Готово.
.slnx создан.Старый формат делали под внутренности Visual Studio, не под людей. SLNX делают для разработчиков.
Все еще держишься за .sln? Почему?
Please open Telegram to view this post
VIEW IN TELEGRAM
👍28❤4🔥1
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13😐9👎1