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

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

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

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

РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
Download Telegram
👨‍💻 Чистые стектрейсы в .NET с помощью StackTraceHiddenAttribute

В .NET есть простой способ скрыть внутреннюю кухню и оставить только то, что важно потребителю API. Атрибут StackTraceHiddenAttribute помечает методы, которые не должны попадать в публичный стектрейс, при этом логика выполнения не меняется.

Пример:
public static class UserService
{
[StackTraceHidden]
private static void ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Name cannot be empty");
}

public static void CreateUser(string name)
{
ValidateName(name);
Console.WriteLine("Created user " + name);
}
}


В этом примере при исключении из ValidateName в стектрейсе вы увидите только вызов UserService.CreateUser. Валидационный хелпер скрыт атрибутом, поэтому внешний разработчик не видит внутренний слой проверки.

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍25🥰31
⚙️ Один обработчик вместо сотни 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
22👍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
👍415🥰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
👍292
💻 Свежий контент для 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
👍32🥰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
🔥7🥰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
👍15🥰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
🔥8👍32🥰1