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

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

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

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

РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
Download Telegram
⚙️ Один обработчик вместо сотни try catch

Когда в проекте десятки эндпоинтов, разъезжающий по коду try catch быстро превращается в свалку. Гораздо проще один раз настроить глобальный маппинг исключений в HTTP статус и возвращать нормальные ProblemDetails для всех ошибок.

ASP.NET уже умеет работать с ProblemDetails из коробки, нужно только включить службу и повесить обработчик ошибок.

В примере вся логика перевода исключений в HTTP ответы живет в одном месте:
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler(appErr =>
{
appErr.Run(async ctx =>
{
var ex = ctx.Features.Get<IExceptionHandlerFeature>()?.Error;

var (status, title) = ex switch
{
ConcurrencyException => (StatusCodes.Status409Conflict, "Concurrency conflict"),
NotFoundException => (StatusCodes.Status404NotFound, "Resource not found"),
_ => (StatusCodes.Status500InternalServerError, "Server error")
};

ctx.Response.StatusCode = status;

await ctx.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = status,
Title = title,
Detail = app.Environment.IsDevelopment() ? ex?.Message : null,
Instance = ctx.Request.Path
});
});
});


Любые новые исключения добавляются через одну запись в switch, без походов по контроллерам, а все ответы об ошибках приходят в едином формате application/problem+json, что упрощает жизнь фронту и интеграциям.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
23👍18
GetElapsedTime вместо ручного Stopwatch шаблона

Многие до сих пор измеряют время в .NET по старинке создают экземпляр Stopwatch, вызывают Start, ждут выполнение и читают Elapsed.

Но есть более простой и аккуратный способ через Stopwatch.GetTimestamp и Stopwatch.GetElapsedTime.

Классический шаблон выглядит так:
long start = Stopwatch.GetTimestamp();

// код, который нужно измерить
await ProcessOrderAsync();

TimeSpan elapsed = Stopwatch.GetElapsedTime(start);


GetElapsedTime вычисляет разницу между текущим timestamp и сохранённым значением и возвращает TimeSpan без создания экземпляра Stopwatch. В результате нет лишней аллокации.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍425🥰4🔥1
👨‍💻 Microsoft показали рабочий рецепт iOS виджетов

В официальном .NET блоге появилась большая статья о том, как собирать iOS виджеты поверх .NET MAUI не теряя нативности.

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

Статья не пошаговый туториал, а набор ключевых шагов и граблей от настроек App Groups и bundle id до интеграции Xcode виджет расширения в MAUI проект.

➡️ Читать статью

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
⚙️Паттерн пагинации и фильтрации через один запрос и нормальные DTO

Пагинация в API часто превращается либо в магию репозиториев, либо в жуткий копипаст. Лучше держать всё предельно простым: один запрос с фильтрами, подсчётом и проекцией в DTO.

Что вообще хотим от пагинации

Когда фронт просит список пользователей, ему нужно не только «20 штук с offset», а нормальные данные

• Текущая страница.
• Размер страницы.
• Список элементов.
• Общее количество записей под этими фильтрами.

Контейнер для любого списка: пользователей, заказов или логов.
public sealed record Paginated<T>(
IReadOnlyList<T> Items,
int Page,
int Size,
int Total
);

• Items то, что реально отображаем.
• Page номер текущей страницы.
• Size сколько элементов на странице.
• Total сколько записей всего под заданным фильтром.

Зачем DTO, а не сущность из EF

• EF сущность часто содержит поля, которые нельзя светить наружу (пароли, внутренние флаги, технические поля).

• DTO можно менять отдельно от внутренней модели база может жить своей жизнью, а API остаётся стабильным.

• EF Core умеет проецировать прямо в DTO через Select, не тянуть все поля сущности и не включать трекинг.

В итоге хендлер не выкидывает наружу сырые сущности, а отдаёт ровно то, что нужно клиенту.

Как выглядит запрос сверху
public sealed record SearchUsersQuery(
string? Q,
int Page = 1,
int Size = 20
) : IRequest<Paginated<UserSummary>>;

• Q — строка поиска.
• Page — номер страницы.
• Size — размер страницы.

Контроллер ничего не знает о базе и EF он просто пробрасывает запрос в Application слой:
[HttpGet]
public async Task<ActionResult<Paginated<UserSummary>>> Search(
[FromQuery] string? q,
[FromQuery] int page,
[FromQuery] int size,
ISender sender,
CancellationToken ct
)
{
var result = await sender.Send(new SearchUsersQuery(q, page, size), ct);
return Ok(result);
}


Что делает хендлер под капотом
public async Task<Paginated<UserSummary>> Handle(
SearchUsersQuery query,
CancellationToken ct)
{
var users = _db.Users.AsQueryable();

if (!string.IsNullOrWhiteSpace(query.Q))
{
var q = query.Q.Trim();
users = users.Where(u =>
u.Email.Contains(q) ||
u.Name.Contains(q));
}

var total = await users.CountAsync(ct);

var items = await users
.OrderBy(u => u.Email)
.Skip((query.Page - 1) * query.Size)
.Take(query.Size)
.AsNoTracking()
.Select(u => new UserSummary(
u.Id,
u.Email,
u.IsActive
))
.ToListAsync(ct);

return new Paginated<UserSummary>(
items,
query.Page,
query.Size,
total
);
}



AsQueryable() чтобы можно было постепенно навешивать фильтры.

• Фильтрация по Q делается в базе, а не в памяти. Email и Name фильтруются прямо в SQL.

​• CountAsync считает Total для уже отфильтрованного набора, без Skip/Take. Это количество строк, которые удовлетворяют фильтрам.

Skip и Take делают пагинацию на стороне БД через OFFSET / FETCH или аналог, а не в памяти приложения.

AsNoTracking() говорит EF Core не отслеживать сущности в change tracker, что ускоряет чистые запросы на чтение.

Select сразу проецирует в UserSummary EF не создаёт полноценные сущности, не подгружает лишние поля и не собирает сложные графы.

Всё это превращается в один адекватный SQL запрос, а не в серию SELECT * плюс ручная фильтрация и подсчёты.

Паттерн пагинации это не про сложность, а наоборот про простоту и предсказуемость. Отдельные DTO, контейнер Paginated<T>, один явный запрос с фильтрами, подсчётом и AsNoTracking() дают API, которое не врёт клиенту, хорошо масштабируется и остаётся читабельным через год.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍294
💻 Свежий контент для Copilot

Проблема с Copilot: он не знает о новых штуках вроде Agent Framework. Решение от Microsoft: MCP сервер дает доступ к свежей .NET-документации прямо в инструментах разработки.

Сервер передает Copilot актуальные данные: гайды, код, уроки. Идеально для .NET 10, Aspire или оптимизации старых приложений. Контекст адаптируется под код.

➡️ Как всё это настроить

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
😁 Он вам не веб-сервис

Вы замечали, что Windows работает не так, как раньше? Это всё из-за новомодных ИИ и веб-элементов. Первое пишет, а второе лагает.

Наткнулись на видео, где всё это выпиливают из актуальной Windows 11. Результат вас не порадует, но зато вы узнаете на что конкретно жаловаться в Microsoft.

➡️ Смотреть видео

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
3👍3🥰1
💃 Волшебство для больших проектов

Partial классы — это C#-хакинг, который разбивает один класс на несколько файлов, не ломая компиляцию. Идеально для автогенерации, командной работы и масштабирования монстров-кода.

Представьте: WinForms или WPF дизайнер генерирует кучу кода в .Designer.cs, а вы хотите добавить свою логику. Без partial пришлось бы вручную мержить изменения или наследоваться с кучей override. Partial решает это элегантно — основной класс в вашем файле, автокод в отдельном.

Все части класса должны иметь одинаковые модификаторы доступа и использовать ключевое слово partial. Компилятор проверит согласованность.

Живой пример:
// Employee.Core.cs — базовая структура
public partial class Employee
{
public string Name { get; set; }
public decimal Salary { get; set; }

public partial void ValidateName();
public partial void ValidateSalary();

public void Hire()
{
ValidateName();
ValidateSalary();
Console.WriteLine($"{Name} нанят с зарплатой {Salary:C}!");
}
}

// Employee.Validation.cs — бизнес-правила
public partial class Employee
{
public partial void ValidateName()
{
if (string.IsNullOrWhiteSpace(Name) || Name.Length < 2)
throw new ArgumentException("Имя должно быть не короче 2 символов!");
}

public partial void ValidateSalary()
{
if (Salary < 50000) throw new ArgumentException("Зарплата не может быть ниже 50k!");
}
}

// Employee.Extensions.cs — расширения (опционально)
public partial class Employee
{
public void Promote() => Salary *= 1.2m;
}


Partial методы уникальны: если реализация отсутствует, метод полностью удаляется из IL. Идеально для опциональных хуков в генерируемом коде.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥8🥰2
🧠 When и Unless для умных проверок

В FluentValidation условная валидация запускает правила только при нужных условиях. When и Unless экономят циклы и делают валидаторы читаемыми.

When = «проверить, если условие верно»
Unless = «проверить, если условие НЕ верно»

Пример:
RuleFor(x => x.ShippingAddress)
.NotEmpty()
.When(x => x.DeliveryMethod == "Express");

RuleFor(x => x.CreditCard)
.NotEmpty()
.Unless(x => x.PaymentMethod == "PayPal");


When проверяет условие перед правилом. Express доставка требует адрес, PayPal не требует карту. Логика в одном месте без if-else в контроллере.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍83
🗂 Работа и пет-проекты с одного ПК

Работаете днём в корпоративном репо, вечером в личном на GitHub. Один ПК, но email и ключи подписи разные. Ручная смена настроек Git каждый раз бесит и приводит к косякам с коммитами.

Условные includeIf решают всё

Добавьте в ~/.gitconfig пару строк. Git сам подхватит нужный конфиг по пути к папке или remote URL.

Для рабочих папок загрузит корпоративный email:
[includeIf "gitdir:~/work/"] path = .gitconfig-corp


Личные репозитории получат ваш домашний email автоматически:
[includeIf "hasconfig:remote.*.url:https://github.com/вашеимя/**"] path = .gitconfig-personal


Забудьте про git config --global user.email каждый раз.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16🥰3🔥2
⚙️ Поиск дубликатов с LINQ

Когда коллекция растет, руками гонять двойные циклы для поиска дубликатов уже не хочется. В .NET это хорошо решается комбинацией LINQ и HashSet, плюс классическими GroupBy и Distinct.

LINQ Any и HashSet

Идея простая: HashSet хранит только уникальные элементы. Метод Add возвращает false если такое значение уже было. Значит можно пробежать коллекцию через Any и остановиться на первом дубликате:
public static bool HasDuplicatesAny<T>(IEnumerable<T> source)
{
HashSet<T> seen = new();

return source.Any(item => !seen.Add(item));
}


Метод возвращает true как только встретится элемент который не удалось добавить в набор.

LINQ GroupBy и Count 1

Если нужны сами дубликаты, а не только факт их наличия, помогает GroupBy. Мы группируем по значению и фильтруем группы в которых больше одного элемента:
public static IEnumerable<T> GetDuplicatesGroupBy<T>(IEnumerable<T> source)
{
return source
.GroupBy(x => x)
.Where(g => g.Count() > 1)
.Select(g => g.Key);
}


LINQ Distinct и подсчет элементов

Иногда достаточно проверить есть ли дубликаты, не вытаскивая их. Тогда можно сравнить размер исходной коллекции и количества уникальных элементов после Distinct:
public static bool HasDuplicatesDistinct<T>(IEnumerable<T> source)
{
int total = source.Count();
int unique = source.Distinct().Count();

return unique != total;
}


Для быстрых проверок чаще всего хватает Any + HashSet. GroupBy удобно когда нужны сами дубликаты.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥13👍421🥰1
⚙️ Yield в упрощает итерацию

Yield позволяет создавать итераторы, возвращая элементы коллекции по одному, без загрузки всего в память сразу.

Как работает yield return

При yield return код возвращает значение и сохраняет состояние для следующей итерации в foreach или LINQ. Компилятор генерирует класс, реализующий IEnumerable<T>, с MoveNext() для продолжения.

Пример простого генератора четных чисел:
IEnumerable<int> EvenNumbers(int max) {
for (int i = 0; i <= max; i += 2) {
yield return i;
}
}


foreach по нему выдаст 0, 2, 4... лениво, только при запросе.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🥱42
🛠 ToonEncoder для .NET

ToonEncoder — это библиотека, которая кодирует данные в TOON. TOON описывается как компактное человекочитаемое представление той же модели данных что и JSON, но с упором на экономию токенов в промптах для LLM.

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

Пример:
using Cysharp.AI;

var users = new User[]
{
new (1, "Alice", "admin"),
new (2, "Bob", "user"),
};

string toon = ToonEncoder.Encode(users);
Console.WriteLine(toon);

string toon2 = ToonEncoder.EncodeAsTabularArray(users);
Console.WriteLine(toon2);

Console.WriteLine(toon == toon2); // same result

public record User(int Id, string Name, string Role);


У TOON есть режимы с разными разделителями, включая таб и пайп, чтобы еще чуть подкрутить токенизацию под конкретный ввод.

➡️ Репозиторий

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍81
🔗 Цепочка конструкторов в 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
🥱97🤩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
👍63🥰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
👍151