Библиотека шарписта | C#, F#, .NET, ASP.NET
22.3K subscribers
2.59K photos
41 videos
85 files
4.87K links
Все самое полезное для C#-разработчика в одном канале.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/b60af5a4

Для обратной связи: @proglibrary_feeedback_bot

РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
Download Telegram
🔗 Цепочка конструкторов в C#

Конструкторы вызываются при создании объекта. В C# можно связывать их в цепочку: один конструктор вызывает другой. Это убирает дублирование кода инициализации.​

Представьте класс User с тремя конструкторами:
public class User
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public DateTime Created { get; set; }

// Базовая инициализация
public User()
{
Created = DateTime.Now;
Name = "Гость";
Email = "guest@example.com";
}

public User(string name)
{
Created = DateTime.Now; // Дублируем!
Name = name;
Email = "guest@example.com";
}

public User(string name, string email)
{
Created = DateTime.Now; // Снова дублируем!
Name = name;
Email = email;
}
}


Проблема: Created и дефолтные значения повторяются. Изменить в одном месте нельзя.

Как работает цепочка с this()

Используйте : this(параметры):
public class User
{
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
public DateTime Created { get; set; }

// Главный конструктор - вся логика здесь
public User(string name, string email, int age = 0)
{
Created = DateTime.Now;
Name = name ?? "Гость";
Email = email ?? "guest@example.com";
Age = age;
}

// Цепочка к главному
public User() : this("Гость", "guest@example.com") { }

public User(string name) : this(name, "guest@example.com") { }

public User(string name, string email) : this(name, email, 0) { }
}


Тестируем:
var user1 = new User();                    // Гость, guest, 0
var user2 = new User("Иван"); // Иван, guest, 0
var user3 = new User("Иван", "i@example"); // Иван, i@example, 0
var user4 = new User("Анна", "a@ex", 25); // Анна, a@ex, 25


Все получают Created = Now автоматически

Правила цепочки:

• Должна быть первой в конструкторе:
public User(string name) : this(name, "default")  // OK
{
// Дополнительная логика
}


• Нельзя дважды:
public User() : this("a") : base()  // Ошибка!


Идеально для классов с множеством опциональных параметров. Главное: короткие цепочки и главный конструктор с полной логикой.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱98🤩5😁1
🌐 Перенаправляем запросы в ASP.NET

Redirect в ASP.NET перенаправляет пользователя на другой URL с кодом 302 Found. Это временное перемещение, когда браузер каждый раз проверяет исходный адрес.

Основные примеры использования:
// 1. Абсолютный путь
return Redirect("https://example.com");

// 2. Относительный путь
return Redirect("/admin/dashboard");

// 3. Действие контроллера
return RedirectToAction("Edit", "User", new { id = model.Id });

// 4. URL с параметрами
return Redirect($"/products?category={categoryId}");


Redirect подходит для временных переходов. Для постоянных ссылок переходите на RedirectPermanent.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
6
🌐 Постоянные редиректы в ASP.NET

RedirectPermanent работает в контроллерах MVC и Core. Метод отправляет статус 301 Moved Permanently, браузеры кэшируют такой редирект и сразу переходят на новый адрес без повторных запросов к старому.

В контроллере это выглядит просто:
public IActionResult OldPage()
{
return RedirectPermanent("/new-page");
}


Здесь запрос на /old-page уйдет на /new-page с кодом 301. Поисковики обновят индекс и перестанут индексировать старый адрес.

Разница с обычным Redirect в статусе ответа. Redirect (302) говорит «временно перемещено», браузер каждый раз проверяет старый URL. RedirectPermanent (301) фиксирует «навсегда», что экономит трафик и улучшает SEO.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍62🥰2🤔1
👨‍💻 Ложная иммутабельность

Часто новички в функциональном программировании думают, что сделали иммутабельный код. Берут список, вызывают ToList() или Select() и радуются новому списку. Но в C# это ловушка.

Проблема в ссылочных типах. var copy = original.ToList() создаёт новый List, но все элементы внутри — те же самые объекты. Меняете свойство через copy, и оригинал тоже меняется.

Правило трёх

• Records с init-only свойствами для value-like поведения

• ImmutableList<T> из System.Collections.Immutable — настоящие неизменяемые коллекции

• Struct только для маленьких типов

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥14😁1🥱1
📅 Даты и время в .NET без сюрпризов

В .NET есть разные типы под разные задачи, и из этого обычно и растут ошибки. DateTime это просто дата и время, но без явного смещения. DateTimeOffset хранит дату, время и смещение от UTC, поэтому однозначно задает момент времени.

Для часовых поясов используйте TimeZoneInfo, Microsoft прямо пишет, что для новой разработки лучше TimeZoneInfo вместо старого TimeZone.

var nowUtc = DateTimeOffset.UtcNow;
var moscow = TimeZoneInfo.FindSystemTimeZoneById("Europe/Moscow");
var nowMoscow = TimeZoneInfo.ConvertTime(nowUtc, moscow);

Console.WriteLine(nowUtc);
Console.WriteLine(nowMoscow);


TimeZoneInfo умеет переводить время между часовыми поясами, а DateTimeOffset делает момент переносимым между машинами и окружениями.

Еще два полезных типа. DateOnly для календарной даты без времени, например отпуск или дата рождения. TimeOnly для времени без даты, например расписание.

var vacation = new DateOnly(2026, 1, 10);
var standup = new TimeOnly(10, 30);

Console.WriteLine(vacation);
Console.WriteLine(standup);


DateOnly и TimeOnly заменяют костыли с DateTime и TimeSpan, когда вам не нужен момент времени.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍181
👀 Сортировать все ради одного значения — это дорогая привычка

В продакшене до сих пор встречается LINQ паттерн, где коллекцию сначала сортируют, а потом берут первый элемент:
var youngest = people
.OrderBy(p => p.Age)
.First();


На вид код нормальный, но по факту он платит за сортировку, хотя нужна всего одна крайняя точка.

Правильнее выразить намерение напрямую через MinBy:
var youngest = people.MinBy(p => p.Age);


MinBy возвращает элемент с минимальным ключом и не требует полной сортировки последовательности. Если в коде встречается OrderBy().First() или OrderByDescending().First(), это повод остановиться и проверить, не ищется ли просто минимум или максимум.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍33🥱5
📎 Когда хочется коротко, но выходит долго

GroupBy в LINQ удобный и выразительный, поэтому его часто тянут в любой код, где надо что то посчитать по ключу.

Проблема в том, что GroupBy решает задачу группировки, а агрегация это частный случай, и за него иногда приходится платить лишним.

Типичный паттерн выглядит так:
var totals = orders
.GroupBy(o => o.CustomerId)
.Select(g => new
{
CustomerId = g.Key,
Total = g.Sum(o => o.Amount)
})
.ToList();


Выглядит читабельно, но GroupBy внутри строит структуру групп, а значит элементы буферизуются и создаются объекты группировок, даже если в итоге нужен только итоговый Total.

Если нужна именно аккумуляция, проще и дешевле сделать один проход и складывать суммы в словарь:
var totals = new Dictionary<int, decimal>();
foreach (var order in orders)
{
if (totals.TryGetValue(order.CustomerId, out var current))
totals[order.CustomerId] = current + order.Amount;
else
totals[order.CustomerId] = order.Amount;
}


Это скучно, зато алгоритм читается буквально, один проход, явные обновления, без сюрпризов с материализацией групп.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍191
🖨 Добавляем картинку в PDF

Разбираемся, как встроить изображение в PDF так, чтобы всё корректно печаталось, с помощью C#.

IronPdf — это библиотека, которая работает по принципу «что видите в HTML, то получите в PDF». Вместо возни с координатами вы просто верстаете страницу как для браузера.

Библиотека платная для коммерческих проектов, но есть пробный период. Для некоммерческих целей можно использовать бесплатно.

Самый простой способ — сгенерировать HTML с тегом <img> и конвертировать в PDF:
using IronPdf;

var renderer = new ChromePdfRenderer();

string html = @"
<html>
<body>
<img src='logo.png' style='width: 200px;' />
<p>Текст под картинкой</p>
</body>
</html>";

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("document_with_image.pdf");


Если картинка приходит из базы данных или веб-формы, удобнее встроить её прямо в HTML через Data URI:
byte[] imageBytes = File.ReadAllBytes("logo.png");
string base64 = Convert.ToBase64String(imageBytes);

string html = $@"
<html>
<body>
<img src='data:image/png;base64,{base64}' style='width: 200px;' />
<h2>Отчёт за декабрь</h2>
</body>
</html>";

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("report.pdf");


Если нужно добавить штамп или подпись на готовый документ, IronPdf позволяет работать со слоями:
var existingPdf = PdfDocument.FromFile("contract.pdf");

// Создаём HTML со штампом
string stampHtml = @"
<div style='position: absolute; bottom: 50px; right: 50px;'>
<img src='approved_stamp.png' style='width: 100px; opacity: 0.7;' />
</div>";

var stampPdf = renderer.RenderHtmlAsPdf(stampHtml);

// Накладываем на каждую страницу
foreach (var page in existingPdf.Pages)
{
page.AddBackgroundPdf(stampPdf);
}

existingPdf.SaveAs("contract_approved.pdf");


Частые ошибки

• Картинка не появляется — проверьте путь к файлу. IronPdf ищет относительно рабочей директории приложения. Используйте Path.GetFullPath() для отладки или встраивайте через Base64.

• Размытые изображения — убедитесь, что исходное разрешение картинки достаточное. Для печати нужно минимум 300 DPI. Если растягиваете маленькое изображение через width в CSS, оно будет мыльным.

• Огромный размер файла — IronPdf встраивает оригиналы. Оптимизируйте картинки заранее: сжимайте JPEG до 80-85% качества, для PNG используйте инструменты вроде TinyPNG.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍145
👨‍💻 Как превратить O(1) в O(n)

Одна из самых неприятных просадок производительности в C# начинается не с LINQ операторов, а с того, что коллекции начинают жить как IEnumerable.

Пример который встречается постоянно:
IEnumerable<Order> orders = GetOrders();
if (orders.Count() > 0)
{
// do something
}


Если внутри реально List или массив, то количество элементов доступно быстро через свойство Count или Length, то есть без прохода по данным.

Но Count() как LINQ метод обязан работать для любого IEnumerable, поэтому в общем случае он перечисляет элементы, пока не посчитает все.

Так проверка на пустоту внезапно становится полным обходом, и это особенно больно если orders это запрос с отложенным выполнением или поток данных.

А если хочется сохранить быстрый путь для коллекций, можно явно проверить интерфейс с Count:
if (orders is ICollection<Order> collection)
{
if (collection.Count > 0)
{
// fast path
}
}
else if (orders.Any())
{
// fallback
}


Если в профилировщике внезапно появился лишний проход по данным, стоит проверить где коллекция превратилась в IEnumerable и где после этого зовется Count().

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱11👍91🤔1😢1
⚡️ Чистый LINQ, грязные аллокации

Проблема LINQ в горячих местах обычно не в вычислениях, а в том, что вокруг каждого запроса появляются маленькие кусочки инфраструктуры.

Where и Select сами по себе ленивые, они не выполняются пока не началось перечисление. Но как только появляется ToArray, начинается реальная работа и выделяется новый массив под результат

Если внутри цикла постоянно вызывается ToArray, то постоянно создаются новые массивы.
Если еще и лямбды захватывают переменные, добавляются лишние замыкания, и это тоже может стать частью мусора, который потом будет собирать GC.

Пример:
foreach (var batch in batches)
{
var validItems = batch
.Where(IsValid)
.Select(Transform)
.ToArray();

Process(validItems);
}


Ручной цикл выглядит скучнее, но дает контроль над буфером и позволяет переиспользовать память:
var buffer = new List<Result>(batch.Count);

foreach (var item in batch)
{
if (!IsValid(item))
continue;
buffer.Add(Transform(item));
}
Process(CollectionsMarshal.AsSpan(buffer));
buffer.Clear();


CollectionsMarshal.AsSpan дает доступ к внутреннему массиву List как Span, то есть можно передать данные дальше без копирования, но важно не менять List пока Span используется.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱7👍5👾51
📎 SelectMany это двойной foreach, просто в красивой упаковке

SelectMany проектирует каждый элемент во вложенную последовательность и потом склеивает эти последовательности в одну общую.

И это отличный выбор для чистой проекции без сложной логики, когда нужно ровно развернуть коллекцию коллекций.

Но если в задаче появляется управление потоком или условия выхода:
var allItems = orders
.SelectMany(o => o.Items)
.Where(i => i.IsActive)
.ToList();


Цикл часто дает больше ясности и предсказуемости:
var allItems = new List<Item>();

foreach (var order in orders)
{
foreach (var item in order.Items)
{
if (!item.IsActive)
continue;
allItems.Add(item);
}
}


Плюс тут легко вставить break, continue, счетчики, ограничения, и не гадать как они сложатся с отложенным выполнением.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔9😁1🥱1
👨‍💻 Когда if начинает распухать

Паттерн стратегия подходит, когда один и тот же сценарий можно выполнить несколькими способами, и выбор зависит от условий. Вместо большого switch внутри сервиса разные варианты выносятся в отдельные классы с общим интерфейсом, а контекст просто делегирует работу выбранной реализации.

Типовая схема такая. Есть интерфейс Strategy, есть несколько конкретных стратегий, и есть контекст, который держит ссылку на стратегию и вызывает ее метод, не зная деталей реализации. Это снижает связность и позволяет добавлять новые варианты без переписывания старого кода.

Мини пример на C#:
public interface IDiscountStrategy
{
decimal Apply(decimal total);
}

public sealed class RegularDiscount : IDiscountStrategy
{
public decimal Apply(decimal total) => total;
}

public sealed class VipDiscount : IDiscountStrategy
{
public decimal Apply(decimal total) => total * 0.9m;
}

public sealed class Checkout
{
private readonly IDiscountStrategy _discount;

public Checkout(IDiscountStrategy discount) => _discount = discount;

public decimal TotalWithDiscount(decimal total) => _discount.Apply(total);
}


Если стратегия выбирается по условиям, условие должно выбирать объект, а не ветку кода:
var checkout = serviceProvider.GetRequiredService<Checkout>();
var total = checkout.TotalWithDiscount(100m);


Выбор стратегии лучше делать на границе приложения, например в фабрике или при конфигурации через DI, потому что прямое создание зависимостей внутри сервиса жестко привязывает код к конкретной реализации.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
12
🧑‍💻 Исключения — не для обычного управления потоком

Исключения во флоу — это не просто дорогие операции. Они дороги когнитивно: затуманивают логику программы и размывают границу между ошибкой и нормальным поведением.

Антипаттерн, который всё ещё встречается:
{
var value = dictionary[key];
Process(value);
}
catch (KeyNotFoundException)
{
// ignore
}


Отсутствие ключа в словаре — это не исключительная ситуация. Это ожидаемый сценарий. Использование exceptions в таких случаях передаёт неправильный смысл и усложняет отладку, когда происходят настоящие ошибки.

Правильный подход — явное ветвление:
if (dictionary.TryGetValue(key, out var value))
{
Process(value);
}


Этот код читается понятно, работает быстрее и сохраняет исключения значимыми.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱96👍5😁2
🛠 Parallel.For — когда циклы работают параллельно

Представьте: у вас есть массив из миллиона элементов, и каждый нужно обработать. Обычный цикл for будет делать это последовательно — один элемент за другим. А что если задействовать все ядра процессора?

Базовое использование:
// Было
for (int i = 0; i < 1000; i++)
{
ProcessImage(images[i]);
}

// Стало
Parallel.For(0, 1000, i =>
{
ProcessImage(images[i]);
});


Вот и всё. Task Parallel Library сам распределит работу по потокам, сам управляет ThreadPool, сам балансирует нагрузку.

Когда это имеет смысл:

+ Обработка изображений, видео
+ Математические вычисления
+ Парсинг больших объёмов данных
+ Криптографические операции


Не подходит:

- Обработка меньше 100 элементов
- Операции с базой данных или API
- Быстрые операции вроде i * 2

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
15🥱2
📎 Преобразование Excel в JSON

Сделаем минимальный код для чтения таблиц для конвертации в JSON.

Подключите NuGet-пакеты FreeSpire.XLS и Newtonsoft.Json. Первая читает .xlsx без зависимостей, вторая формирует JSON.

Код начинается с загрузки файла в поток. Библиотека преобразует лист в DataSet, где строки становятся объектами. Первая строка служит заголовками полей.

Чтение и конвертация данных:
using Spire.Xls;
using Newtonsoft.Json;

Workbook wb = new Workbook();
wb.LoadFromFile("данные.xlsx");
DataTable таблица = wb.Worksheets.ExportDataTable();

JsonSerializerSettings опции = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore
};

string результат = JsonConvert.SerializeObject(таблица, опции);
File.WriteAllText("выход.json", результат);


Игнорируйте null для компактности. Форматируйте даты единообразно. Тестируйте на реальных данных — библиотека FreeSpire.XLS ограничена объемом, но достаточна для большинства случаев.

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🥱3
🤓 Группировка данных без лишних действий

ToLookup — это метод LINQ, который группирует элементы коллекции по ключу. Похож на GroupBy, но есть важные отличия, которые делают его удобным в определённых ситуациях.

Как работает ToLookup

Метод создаёт структуру данных 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

Подходит, если вам нужно:

+ Многократно обращаться к одним и тем же группам
+ Избежать повторных вычислений группировки
+ Получать пустые последовательности вместо исключений для отсутствующих ключей
+ Неизменяемую структуру данных

Не подходит, если:

- Данные большие и в памяти не поместятся
- Нужна отложенная обработка
- Группировка нужна один раз — тогда GroupBy эффективнее

📍 Навигация: ВакансииЗадачиСобесы

🐸Библиотека шарписта

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
12👍5