.NET Разработчик
6.6K subscribers
446 photos
4 videos
14 files
2.15K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2471. #ЗаметкиНаПолях
Вы Всё Ещё Используете new Random()? Окончание

Начало

Проблема с Random в .NET Framework
Основных проблем с Random в .NET Framework две:
1. При вызове new Random() в .NET Framework он использует системные часы для посева;
2. Системные часы имеют конечное разрешение.
Т.е. при быстром последовательном вызове new Random() в .NET Framework могут возникнуть два экземпляра Random с одинаковым начальным значением, что означает, что экземпляры Random вернут идентичную последовательность чисел:
// <TargetFramework>net48</TargetFramework>
Parallel.For(0, 10, x =>
{
Random rnd = new();
var value = rnd.Next();
Console.WriteLine(value);
});

При выполнении этого кода несколько чисел (а то и все) могут быть одинаковыми, а это не то, чего мы хотим.

Решение
Создать один экземпляр Random, который используется исключительно для предоставления начальных значений для остальных экземпляров Random. Поскольку начальные значения случайны, экземпляры Random не коррелируют, и вы можете полностью избежать описанной выше проблемы. Этот подход к инициализации используется по умолчанию в .NET Core, поэтому проблема возникает только в .NET Framework.

Мы можем реализовать тот же подход в классе-обёртке ThreadSafeRandom, показанном ниже. Этот класс использует [ThreadStatic], избегая проблем с инициализацией в .NET Framework:
internal static class ThreadSafeRandom
{
[ThreadStatic]
private static Random? _local;
private static readonly
Random Global = new Random();

private static Random Instance
{
get
{
if (_local is null)
{
int seed;
// избегаем конкурентного доступа
lock (Global)
{
seed = Global.Next();
}

_local = new Random(seed);
}
return _local;
}
}

public static int Next() => Instance.Next();
}

Эту потокобезопасную реализацию, можно использовать во всех версиях фреймворка:
Parallel.For(0, 10, x =>
{
var value = ThreadSafeRandom.Next();
Console.WriteLine(value);
});


Если вы используете .NET6+, рекомендуется использовать встроенный Random.Shared. В более ранних версиях вы можете использовать приведённый выше ThreadSafeRandom для решения этих проблем. Если вы ориентируетесь как на .NET6+, так и на .NET Framework, можно использовать директивы #if для разделения реализации в зависимости от целевой среды.

Источник: https://andrewlock.net/building-a-thread-safe-random-implementation-for-dotnet-framework/
👍11
День 2472. #ЗаметкиНаПолях
AsNoTrackingWithIdentityResolution в EF
При работе с Entity Framework Core понимание поведения отслеживания изменений критически важно как для производительности, так и для согласованности данных. Про AsNoTracking() знают все, а вот его мощная альтернатива AsNoTrackingWithIdentityResolution() не так популярна.

Что такое AsNoTracking()?
По умолчанию EF Core отслеживает все сущности, возвращаемые запросами в трекере изменений. Отслеживание обеспечивает:
- автоматическое обнаружение изменений в сущностях;
- операции обновления без явного присоединения сущностей;
- разрешение идентификаторов (гарантируя наличие только одного экземпляра каждой сущности в памяти).

Однако отслеживание сопряжено с накладными расходами. При выполнении операций только чтения, когда не требуется обновлять данные, AsNoTracking() повышает производительность, полностью отменяя отслеживание изменений:
var customers = await context.Customers
.AsNoTracking()
.ToListAsync();


Проблема - дублирование экземпляров сущностей. Если трекер изменений не будет выполнять разрешение идентификаторов, в памяти может оказаться несколько экземпляров одной и той же сущности, например:
var orders = await context.Orders
.AsNoTracking()
.Include(o => o.Customer)
.Where(o => o.OrderDate > DateTime.Now.AddDays(-30))
.ToListAsync();

Если несколько заказов принадлежат одному и тому же клиенту, вы получите отдельные экземпляры Customer для каждого заказа, даже если они представляют одну и ту же запись в БД. Это означает:
- увеличение потребления памяти,
- возможность путаницы при сравнении сущностей,
- потерю ссылочной целостности в графе объектов.

AsNoTrackingWithIdentityResolution()
AsNoTrackingWithIdentityResolution() решает эту проблему, объединяя преимущества двух подходов:
- отсутствие отслеживания изменений (выигрыш в производительности),
- разрешение идентификаторов (гарантия единственности экземпляров сущностей).
var orders = await context.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Customer)
.Where(o => o.OrderDate > DateTime.Now.AddDays(-30))
.ToListAsync();
// Теперь все заказы от одного клиента
// будут ссылаться на один экземпляр Customer в памяти


При этом EF Core:
- выполняет запрос без присоединения сущностей к трекеру изменений;
- сохраняет временную карту идентификации во время материализации запроса;
- гарантирует, что сущности с одинаковым ключом ссылаются на один и тот же экземпляр;
- удаляет карту идентификации после завершения запроса.
Т.е. что вы получаете разрешение идентификации во время выполнения запроса без дополнительных затрат на отслеживание изменений.

Также можно установить AsNoTrackingWithIdentityResolution() как поведение трекера по умолчанию:
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(connectionString)
.UseQueryTrackingBehavior(
QueryTrackingBehavior.NoTrackingWithIdentityResolution);
}


Когда использовать
1. Отслеживание по умолчанию:
- необходимо обновлять, удалять или отслеживать изменения сущностей;
- небольшие наборы результатов, где затраты на отслеживание незначительны;
- необходимо автоматическое обнаружение изменений.

2. AsNoTracking:
- операции только чтения;
- нет свойств навигации или связанных сущностей;
- максимальная производительность критически важна;
- результаты не содержат дублирующихся сущностей.

3. AsNoTrackingWithIdentityResolution:
- операции только чтения с include/join;
- требуется ссылочная целостность в графе объектов;
- запросы, возвращающие одну сущность несколько раз;
- требуется сравнивать сущности по ссылке.

Источник: https://bartwullems.blogspot.com/2025/10/understanding-asnotrackingwithidentityr.html
👍42
День 2473. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы). Я решил разобрать их тут. Начнём с 4го, поскольку первые 3 приведены в его книге, обзор на которую я недавно выкладывал.

7. Делегаты и события

«Можете ли вы объяснить разницу между делегатами и событиями в C# и как они используются для реализации паттерна Наблюдатель? Приведите пример сценария, в котором вы бы их использовали».

Хороший ответ
«В C# делегаты — это объекты, ссылающиеся на методы. Это типобезопасные указатели на функции, позволяющие передавать методы в качестве параметров. Делегаты используются для определения методов обратного вызова и реализации обработки событий в .NET. Событие, с другой стороны, — это механизм, который использует делегаты для предоставления уведомлений о важных событиях в системе. События, по сути, являются обёртками для делегатов, добавляющими уровень защиты; они могут быть вызваны только из класса, в котором они объявлены, что предотвращает прямую активацию события внешними объектами.

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

Например, в UI-приложении можно использовать событие для уведомления компонентов UI об изменении модели данных. Можно определить делегат, определяющий сигнатуру методов, которые могут реагировать на событие, и событие, основанное на этом делегате. Компоненты UI, которым необходимо обновляться при изменении модели, могут подписаться на это событие и реагировать соответствующим образом.

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


Часто встречающийся некорректный ответ
«Делегаты и события в C# — это одно и то же; они используются для указания методов, которые можно вызвать позже. Я использую их взаимозаменяемо в своих приложениях для запуска методов из других классов».

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

- Путаница между делегатами и событиями: В этом ответе не проводится должного различия между делегатами и событиями. Хотя оба метода действительно используют указатели на методы, события — это особый вид применения делегатов, предназначенный для обеспечения инкапсуляции и предоставления стандартного шаблона обработки событий. События защищают список вызовов делегата, гарантируя, что только класс-владелец может вызвать событие.

- Неправильное использование делегатов и событий: Заявление об их взаимозаменяемости упускает из виду цель событий — инкапсуляцию экземпляра делегата и безопасное управление списками подписок. Такое неправильное использование может привести к неудачному выбору архитектуры, когда делегаты могут быть доступны публично, что может поставить под угрозу целостность и безопасность приложения.

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

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
1👍24
День 2474. #ЗаметкиНаПолях #Git
Использование Условных Включений Git для Нескольких Конфигураций

При работе с несколькими репозиториями Git часто требуются разные настройки для каждого контекста. Например, вы можете использовать личную электронную почту для пет-проектов и рабочую — для корпоративных. Хотя Git можно настроить глобально или для каждого репозитория, ручное управление этими настройками становится утомительным и подверженным ошибкам.

Функция условных включений Git решает эту проблему, автоматически применяя различные конфигурации на основе таких критериев, как расположение репозитория, удалённый URL или имя ветки. Это гарантирует, что правильные настройки всегда будут использоваться без ручного вмешательства.

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

Когда Git обнаруживает удалённый URL, соответствующий этому шаблону, он автоматически добавляет настройки из файла .gitconfig-github-personal, который может содержать ваш личный email и ключ подписи.

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

1. gitdir — Сопоставление по пути к репозиторию
Условие gitdir сопоставляет репозитории на основе пути к их файловой системе. В Unix-подобных системах оно чувствительно к регистру.
[includeIf "gitdir:~/personal/"]
path = .gitconfig-personal

[includeIf "gitdir:~/work/"]
path = .gitconfig-work

Это применит .gitconfig-personal ко всем репозиториям в ~/personal/ и .gitconfig-work – к репозиториям в ~/work/.
gitdir/i аналогичен для нечувствительного к регистру сопоставления.

2. onbranch – Сопоставление по текущей ветке
Условие onbranch применяет конфигурацию на основе ветки, для которой сделан checkout:
[includeIf "onbranch:main"]
path = .gitconfig-production

[includeIf "onbranch:feature/**"]
path = .gitconfig-development

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

3. hasconfig — Сопоставление с существующей конфигурацией
Условие hasconfig проверяет существование определённого значения конфигурации и его соответствие шаблону. Это особенно полезно для сопоставления удалённых URL:
[includeIf "hasconfig:remote.*.url:https://github.com/myname/*"]
path = .gitconfig-github-personal

[includeIf "hasconfig:remote.*.url:[email protected]:*/**"]
path = .gitconfig-company

Это позволяет применять различные конфигурации в зависимости от того, где размещён ваш репозиторий, независимо от его локального пути.

Источник: https://www.meziantou.net/using-git-conditional-includes-for-multiple-configurations.htm
👍23
День 2475. #SystemDesign101
Типы Серверов в Современных Системах

Источник: https://blog.bytebytego.com
👍16👎1
День 2476. #ЗаметкиНаПолях
Больше Никаких Моков ILogger'a
Имитация зависимостей в процессе разработки может быть сложной задачей, особенно если вы не являетесь владельцем интерфейса, а взаимодействие с интерфейсом в коде осуществляется через методы расширения. Так происходит с ILogger из базового пакета Microsoft. Интерфейс ILogger относительно прост, однако вы редко взаимодействуете с ним напрямую. Как же проверить, что был вызван правильный метод и параметры, чтобы гарантировать корректность ведения журнала?

Можно использовать мок ILogger<> с помощью Moq и внедрить его в тестируемую систему, а затем проверять, вызывается ли он. Однако это может быть затруднительно, и также потребуется копировать код проверки мока. Это неидеально.

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

Начало работы
С момента создания он немного изменился. Сейчас его можно найти в пакете Microsoft.Extensions.Diagnostics.Testing. После добавления NuGet-пакета, его можно использовать в модульных тестах. Рассмотрим простой сервис в качестве тестируемой системы:
public class DemoService(ILogger<DemoService> logger)
{
public void LogMe()
{
logger.LogInformation("Пишем в лог.");
}
}

В сервисе выше мы хотим проверить, что при выполнении метода LogMe вызывается соответствующий метод логирования.

Начнём с создания экземпляра тестируемой системы, а для этого необходимо создать экземпляр FakeLogger:
var fakeLogger = new FakeLogger<DemoService>();
var sut = new DemoService(fakeLogger);

Т.к. FakeLogger<> реализует нужный интерфейс, мы можем использовать его для создания нашего DemoService.

После выполнения блока Act https://t.iss.one/NetDeveloperDiary/1003 теста мы можем проверить результаты:
fakeLogger
.LatestRecord
.Message
.Should()
.Be("Пишем в лог.");

Самый простой способ сделать это — посмотреть свойство LatestRecord. Это выведет последний вызов экземпляра логгера в тестируемой системе, и это всё, что нужно для базового использования!

Итого
FakeLogger удобно использовать при тестировании системы, требующей реализации ILogger. Он позволяет тестировать корректные уровни логирования, сообщения и структурированные данные, указанные в системных сообщениях логгера.

Источник: https://adamstorr.co.uk/blog/no-more-mocking-ilogger/
👍36
День 2477. #ЗаметкиНаПолях
Тестирование с Использованием FakeLogger
Вместо того, чтобы заниматься сложными настройками моков или проверять вызовы вручную, FakeLogger собирает сообщения журнала, области действия и структурированные данные в памяти, делая утверждения простыми и выразительными. Сегодня рассмотрим, как тестировать области логирования с помощью FakeLogger, зачем нужны области и как использовать их в коде.

Зачем использовать области действия?
Область логирования в .NET позволяет прикреплять контекстную информацию ко всем записям журнала в блоке кода. Представьте себе облегчённый набор пар «ключ-значение», который дополняет сообщения журнала, не заставляя вас предоставлять одни и те же поля в каждую запись журнала.

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

Использование FakeLogger с областями действия
Начнём с простого метода, использующего область действия журналирования:
public partial class 
DemoService(ILogger<DemoService> lgr)
{
public void LogMe(Guid id)
{
using var _ =
lgr.BeginScope(
new Dictionary<string, object> { { "id", id } });

lgr.LogInformation("Пишем в лог.");
}
}

Здесь вызов BeginScope создаёт контекст логирования с единственным ключом (id). При вызове lgr.LogInformation область действия автоматически включает эти контекстные данные. Но как это проверить в тесте?

FakeLogger отслеживает каждую запись журнала, включая шаблоны сообщений, уровни ведения журнала, исключения и области действия. Проверим, правильно ли DemoService пишет в лог, включая id:
public class DemoServiceTests
{
[Fact]
public void ScopeUsage()
{
var fakeLogger = new FakeLogger<DemoService>();
var sut = new DemoService(fakeLogger);
var id = Guid.NewGuid();

sut.LogMe(id);

fakeLogger
.LatestRecord
.Message
.Should()
.Be("Пишем в лог.");

var scope = fakeLogger
.LatestRecord
.Scopes[0] as Dictionary<string, object>;
scope.Should().NotBeNull();
scope.Keys.Should().Contain("id");
scope.Values.Should().Contain(id);
}
}

В этом тесте мы:
- Создаём экземпляр тестируемой системы (SUT), используя FakeLogger<DemoService>.
- Вызываем метод LogMe(), генерирующий одну запись журнала.
- Проверяем содержимое сообщения и проверяем, записана ли коллекция из области действия.

Каждая область действия регистрируется как объект, и в данном случае это Dictionary<string, object>, содержащий id. Этот подход понятен, выразителен и поддерживается фреймворком, устраняя необходимость в пользовательских оболочках для логирования или ненадёжных настройках верификации моков.

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

Источник: https://adamstorr.co.uk/blog/testing-with-fakelogger-scopes/
👍7
День 2478. #ЗаметкиНаПолях
Ограничение Доступа к Методу Действия в
ASP.NET Core MVC. Начало
Рассмотрим несколько вариантов ограничения доступа к определённому методу действия (или ко всем методам) в контроллере ASP.NET Core MVC.

Мы всегда должны добавлять поддержку аутентификации (кто пользователь) и авторизации (что он может делать) в конвейер ASP.NET Core. Авторизация требует аутентификации, но аутентификация может существовать и сама по себе в определённых схемах аутентификации:
builder.Services.AddAuthentication()
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();


Использование фильтров
Фильтры ASP.NET Core хорошо известны и популярны, и, пожалуй, это самый простой способ ограничить доступ к конечной точке. Фильтры можно применять:
- через атрибут, к методу действия или контроллеру,
- глобально для всех контроллеров и действий.

Пользовательские фильтры
IAuthorizationFilter (IAsyncAuthorizationFilter) — интерфейс, определяющий правила авторизации для данной конечной точки. Его можно реализовать с помощью атрибута, который затем применить к методу действия, классу контроллера или глобально ко всем запросам:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class DayOfWeekFilterAttribute(
params DayOfWeek[] daysOfWeek)
: Attribute, IAsyncAuthorizationFilter
{
public Task OnAuthorizationAsync(
AuthorizationFilterContext context)
{
if (!(daysOfWeek ?? [])
.Contains(DateTime.Today.DayOfWeek))
context.Result = new ForbidResult();

return Task.CompletedTask;
}
}


Использование:
[DaysOfWeek(DayOfWeek.Saturday, DayOfWeek.Sunday)]
public IActionResult Index() { … }


Возврат результата осуществляется через свойство Result объекта AuthorizationFilterContext:
- ForbidResult: HTTP 403 Forbidden;
- RedirectResult, RedirectToActionResult, RedirectToPageResult, RedirectToRouteResult, LocalRedirectResult: различные виды перенаправлений (HTTP 3xx);
- EmptyResult: ничего с кодом 200 OK;
- StatusCodeResult: пользовательский код статуса.

Продолжение следует…

Источник:
https://developmentwithadot.blogspot.com/2025/10/restricting-access-to-action-method-in.html
👍15
День 2479. #ЗаметкиНаПолях
Ограничение Доступа к Методу Действия в
ASP.NET Core MVC. Продолжение
Начало

Атрибут Authorize
Существует также встроенный атрибут [Authorize], который может быть применён к классам контроллеров или методам действий, но не реализует ни один из этих интерфейсов. Он допускает несколько ограничений:
- Политика: ограничение по именованной политике, которая должна быть определена.
- Роли: одна или несколько ролей, разделённых запятыми. Текущий пользователь должен иметь одну из указанных ролей. Роли берутся из утверждений запроса.

Возможно наличие нескольких атрибутов [Authorize]:
- если в одном [Authorize] несколько ролей, пользователю необходимо иметь одну из них;
- если в нескольких [Authorize] заданы роли, пользователь должен иметь их все.
То же относится и к политикам.

Атрибут [AllowAnonymous], если присутствует, обходит любой атрибут [Authorize].

[Authorize(Roles = "Admin")] 
//все методы действия контроллера требуют роли "Admin"
public class AdminController
: Controller
{
public IActionResult Index()
{ … } //требует роли "Admin"

[AllowAnonymous]
public IActionResult SignOut()
{ … } //может быть вызван кем угодно
}


Если мы хотим использовать именованные политики, мы должны определить их:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminPolicy", p =>
{
p.RequireRole("Admin");
p.RequireAuthenticatedUser();
});


Теперь можно использовать политику "AdminPolicy":
[Authorize(Policy = "AdminPolicy")]
public IActionResult Restricted()
{ … } //требует удовлетворять политике "AdminPolicy"


Роли и политики
Это разные способы управления доступом. Роли напрямую сопоставляются с требованиями аутентификации или группами пользователей, в зависимости от используемого типа аутентификации. Политики же позволяют настраивать требования, которые могут включать роли, но не ограничиваются этим. В определении политики мы можем требовать:
- аутентификации: RequireAuthenticatedUser();
- утверждений (claim): RequireClaim();
- одной из ролей: RequireRole();
- определённого имени: RequireUserName();
- определённого условия: RequireAssertion().
А также:
- Объединять несколько политик: Combine();
- Добавлять требования: AddRequirements().

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

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

Окончание следует…

Источник:
https://developmentwithadot.blogspot.com/2025/10/restricting-access-to-action-method-in.html
👍5
День 2480. #ЗаметкиНаПолях
Ограничение Доступа к Методу Действия в
ASP.NET Core MVC. Окончание
Начало
Продолжение

Использование обработчиков авторизации
Обработчик авторизации — это реализация IAuthorizationHandler, в виде абстрактного класса AuthorizationHandler<TRequirement>, который принимает требование в качестве параметра. Внутри его метода HandleRequirementAsync мы можем реализовать любую логику, передавая требование в качестве параметра:
public record DayOfWeekRequirement(DayOfWeek Day)
: IAuthorizationRequirement
{
}

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

Подключаем требование к обработчику:
public class DayOfWeekAuthHandler 
: AuthorizationHandler<DayOfWeekRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext ctx,
DayOfWeekRequirement req)
{
if (DateTime.Today.DayOfWeek == req.Day)
ctx.Succeed(req);
else
ctx.Fail(new AuthorizationFailureReason(this, "Неправильный день недели"));

return Task.CompletedTask;
}
}


Простой пример выше, принимает в качестве параметра только день недели, но вариантов ограничений бесконечное множество:
- по IP,
- по наличию/отсутствию cookie,
- по заголовкам запроса…

Внутри HandleRequirementAsync запрос либо завершается неудачей (Fail), либо успехом (Succeed). Если зарегистрировано много обработчиков, все они должны успешно завершиться для получения разрешения на доступ к конечной точке. Для всех обработчиков вызывается один и тот же экземпляр параметра AuthorizationHandlerContext, и оттуда можно проверить:
- текущего аутентифицированного пользователя (User),
- контекст (Resource),
- текущий статус запроса (HasFailed, HasSucceeded),
- обработанные требования (Requirements)
- ожидающие обработки требования (PendingRequirements),
- причины отказа (FailureReasons).

Обработчики необходимо зарегистрировать в DI. ASP.NET Core находит соответствующий обработчик, проверяя переданное требование:
builder.Services
.AddSingleton<IAuthorizationHandler, DayOfWeekAuthHandler>();


Мы должны добавить требования в именованную политику:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("DayOfWeekPolicy", p =>
{
p.Requirements.Add(new DayOfWeekRequirement(DayOfWeek.Saturday));
p.Requirements.Add(new DayOfWeekRequirement(DayOfWeek.Sunday));
});


Теперь мы можем добавить атрибут [Authorize], который ссылается на новую политику:
[Authorize(Policy = "DayOfWeekPolicy")]
public IActionResult Index() { … }


Сравнение альтернатив
С [Authorize] мы можем использовать только политики или роли. Определение политики обеспечивает гораздо большую гибкость, поскольку мы можем указать именно то, что нужно, и это можно изменить в любое время. Тем не менее, обработчики авторизации — более отказоустойчивое и универсальное решение, позволяющее инкапсулировать несколько условий с параметрами, а также использовать DI.

Источник: https://developmentwithadot.blogspot.com/2025/10/restricting-access-to-action-method-in.html
👍10
День 2481. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

8. Language Integrated Query (LINQ)
«Расскажите, что такое LINQ и как его можно использовать в приложениях .NET? Приведите примеры различных типов поставщиков LINQ и объясните сценарий, в котором использование LINQ повышает читаемость и эффективность кода».

Хороший ответ
«LINQ, или Language Integrated Query, — это набор технологий, основанных на интеграции возможностей запросов непосредственно в язык C#. LINQ представляет шаблоны для извлечения и обновления данных из различных источников с использованием общего синтаксиса. Он абстрагирует базовый источник данных, позволяя разработчикам использовать один и тот же подход к запросам к БД, XML-документам и коллекциям в памяти.

В .NET существует несколько поставщиков LINQ, каждый из которых предназначен для доступа к различным типам данных:
- LINQ to Objects — используется для запросов к коллекциям в памяти, таким как списки или массивы, реализующие интерфейс IEnumerable.
- LINQ to SQL — предназначен для запросов к базам данных SQL непосредственно из C#.
- LINQ to XML (ранее известный как XLINQ) — предоставляет простые в использовании возможности для запросов и работы с XML-документами.
- LINQ to Entities — часть Entity Framework Core, которая позволяет выполнять запросы к реляционным БД и облачным хранилищам данных через ORM EF Core.

Пример использования LINQ для повышения читабельности кода и его эффективность можно оценить в задачах преобразования данных. Например, представим сценарий, в котором нужно отфильтровать и упорядочить список сотрудников по их зарплате. Без LINQ пришлось бы написать несколько строк императивного кода, включая циклы и условные операторы. С LINQ это можно сделать краткого и ясно в одну строчку:
```csharp
var highSalaryEmployees = employees
.Where(e => e.Salary > 150000)
.OrderBy(e => e.Salary);
```
Это не только сокращает объём кода, но и улучшает его читаемость, чётко выражая его назначение.»


Часто встречающийся неправильный ответ
«LINQ — это способ написания SQL-запросов на C#. Он используется, когда нужно выполнять запросы к базам данных, и работает практически так же, как SQL.»

Этот ответ демонстрирует ограниченное понимание LINQ и упускает из виду его более широкие области применения и преимущества:

- Непонимание области применения LINQ
LINQ предоставляет унифицированную модель для запросов к данным в памяти (например, массивам или спискам), XML и даже к наборам данных из API или других внешних сервисов данных, выходящую далеко за рамки простого взаимодействия с БД.

- Упрощение возможностей
Сравнение с SQL упускает из виду интегрированную природу LINQ в C# и его способность беспрепятственно работать с объектно-ориентированным программированием, предлагая такие функции, как отложенное выполнение и строгая типизация, которые выходят за рамки традиционных возможностей SQL.

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍15👎2
День 2482. #SystemDesign101
Современные Сетевые Сервисы
Каждый раз, когда вы открываете браузер, отправляете электронное письмо или подключаетесь к VPN, эти сетевые сервисы незаметно обеспечивают вашу работу по сети.

DNS: Преобразует доменные имена в IP-адреса, чтобы пользователи могли заходить на веб-сайты без необходимости запоминать цифры.

DHCP: Автоматически назначает IP-адреса и сетевые настройки устройствам, подключающимся к сети.

NTP: Синхронизирует часы между системами, обеспечивая единообразие журналов и аутентификации.

SSH: Обеспечивает безопасный удалённый вход в систему и зашифрованную передачу файлов через порт 22.

RDP: Обеспечивает удалённый доступ к рабочему столу систем Windows через порт 3389.

Почта (SMTP-отправка): Обеспечивает безопасную отправку электронных писем от клиентов на почтовые серверы.

HTTPS/HTTP3 (QUIC): Обеспечивает безопасность веб-сайтов, API и обмена данными между приложениями по зашифрованным каналам.

LDAP (по TLS): Выступает в качестве центрального каталога для корпоративных учётных записей и контроля доступа.

OAuth2.0/OpenID Connect: Обеспечивает современные процессы аутентификации, такие как «Войти с помощью Google».

MySQL/PostgreSQL/Oracle: Управляет хранением и извлечением данных на бэкенде для веб- и мобильных приложений.

WireGuard/IPsec: Создаёт зашифрованные туннели для приватного и удалённого доступа к сети.

Источник: https://blog.bytebytego.com
👍6
День 2483. #ЗаметкиНаПолях
Сжатие Запросов HttpClient с Помощью GZIP
Если вам нужно отправлять большие файлы во внешний API, вы можете решить, что необходимо сжимать их перед отправкой. Однако дело в том, что очевидное решение может привести к проблемам с памятью…

Рассмотрим такой код:
internal async ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
var outStream = new MemoryStream();
await using (var zip =
new GZipStream(outStream,
CompressionMode.Compress,
leaveOpen: true))
{
await source.CopyToAsync(zip);
zip.Close();
}
outStream.Position = 0;

var request = new HttpRequestMessage(
HttpMethod.Post, uri)
{
Content = new StreamContent(outStream)
};
request.Content.Headers.ContentEncoding.Add("gzip");
return request;
}


Использовать его можно примерно так:
var uri = new Uri("https://my.api/endpoint");
var file = File.OpenRead("largefile.txt");
var request = await CreateRequest(uri, file);
await httpClient.SendAsync(request);

Обычно этот код просто работает. Однако каждый раз при отправке запроса сжатый gzip-контент создаётся в памяти (благодаря MemoryStream) перед отправкой. И если требуется отправлять много таких запросов, приложение начнёт испытывать нехватку памяти.

Решение в gzip-сжатии «на лету»:
public class GzipContent : HttpContent
{
private readonly Stream _source;

public GzipContent(Stream source)
{
_source = source;
Headers.ContentEncoding.Add("gzip");
}

protected override async Task
SerializeToStreamAsync(
Stream stream,
TransportContext? context)
{
await using var zip =
new GZipStream(stream,
CompressionMode.Compress,
leaveOpen: true);
await _source.CopyToAsync(zip);
}

protected override bool
TryComputeLength(out long length)
{
length = -1;
return false;
}
}

Также переделаем CreateRequest:
internal ValueTask<HttpRequestMessage>
CreateRequest(Uri uri, Stream source)
{
return ValueTask.FromResult(
new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new GzipContent(source)
});
}

Использование будет точно таким же.

Производительность
Вот мои тесты в .NET10. По времени особой разницы нет, зато версия GzipContent выделяет гораздо меньше памяти. Чем больше файл, тем заметнее разница.
| File | Method| Mean   | Ratio| Allocated| Alc Rat |
|-----:|-------|-------:|-----:|---------:|--------:|
| 7KB| Memory| 633.3us| 1.00| 6.35KB| 1.00|
| 7KB| Gzip | 603.5us| 0.96| 4.77KB| 0.75|
| 200KB| Memory| 8.025ms| 1.00| 508.52KB| 1.000|
| 200KB| Gzip | 7.260ms| 0.90| 4.74KB| 0.009|
| 3MB| Memory| 94.97ms| 1.00| 8189.99KB| 1.000|
| 3MB| Gzip | 86.02ms| 0.91| 4.82KB| 0.001|


PS: .NET API может автоматически распаковывать запросы. Достаточно добавить соответствующее промежуточное ПО:
builder.Services.AddRequestDecompression();

// …

app.UseRequestDecompression();


Источник: https://josef.codes/compress-httpclient-requests-with-gzip-dotnet-core/
👍14
День 2484. #ЗаметкиНаПолях
Разбираем Однофайловые Приложения в .NET 10. Начало
C# всегда был немного перегружен церемониями. Даже простейший «Hello World» традиционно требовал файла решения, файла проекта и достаточного количества шаблонного кода, чтобы заставить вас задуматься, а не лучше ли использовать скриптовый язык. В .NET 10 представлены однофайловые приложения.

Что это?
Идея проста: пишете код C# в одном CS-файле и запускаете его напрямую. Не нужно настраивать структуру целого проекта для быстрого служебного скрипта.

Все прелести C# сохраняются: типобезопасность, производительность, богатая стандартная библиотека. Но теперь вы можете использовать его для тех одноразовых скриптов, для которых создание полноценного проекта было бы излишним. По-прежнему можно ссылаться на NuGet-пакеты, на другие проекты C#, использовать конкретные SDK и настраивать свойства проекта — всё это из одного файла с помощью специальных директив.

Функционал основан на операторах верхнего уровня и доводит их до логического завершения. Если уж мы позволяем пользователям пропускать церемонию создания класса и метода Main, почему бы не позволить им также пропускать церемонию создания проекта?

Начало работы
Предположим, нужно быстро проверить, на какой день недели приходится определённая дата. Создадим файл с date-checker.cs:
var targetDate = new DateTime(2025, 12, 31);
Console.WriteLine(
$"Новый год в 2025 - это {targetDate.DayOfWeek}");
Console.WriteLine(
$"Через {(targetDate - DateTime.Today).Days} дней");

Выполним это с помощью:
dotnet run date-checker.cs

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

Пример из реальной жизни
Допустим, нужно быстро обработать JSON и сформировать отчёт. Используем System.Text.Json и CsvHelper:
#:package [email protected]
using System.Text.Json;
using CsvHelper;
using System.Globalization;
using System.Text.Json.Serialization.Metadata;

var json = await
File.ReadAllTextAsync("sales_data.json");
var sales =
JsonSerializer.Deserialize<List<SaleRecord>>(
json,
new JsonSerializerOptions() {
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
});

var topProducts = sales
.GroupBy(s => s.Product)
.Select(g => new {
Product = g.Key,
TotalRevenue = g.Sum(s => s.Amount),
UnitsSold = g.Count()
})
.OrderByDescending(p => p.TotalRevenue)
.Take(10);

using var writer =
new StreamWriter("top_products.csv");
using var csv =
new CsvWriter(writer, CultureInfo.InvariantCulture);
csv.WriteRecords(topProducts);

Console.WriteLine(
"Отчёт сохранён в top_products.csv");

record SaleRecord(string Product, decimal Amount, DateTime Date);

Заметьте, как мы используем и ссылки на NuGet-пакеты, и асинхронные операции, и LINQ, и записи — всё в одном файле, который читается как связный скрипт. Обычно для таких задач используется Python, но теперь можно остаться в C#.

Окончание следует…

Источник:
https://www.milanjovanovic.tech/blog/exploring-csharp-file-based-apps-in-dotnet-10
👍32
День 2485. #ЗаметкиНаПолях
Разбираем Однофайловые Приложения в .NET 10. Окончание

Начало

Создаём что-то более амбициозное с Aspire
Даже AppHost для Aspire можно создать в одном файле:
#:sdk [email protected]
#:package [email protected]

var builder =
DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache")
.WithDataVolume();

var postgres = builder.AddPostgres("postgres")
.WithDataVolume()
.AddDatabase("tododb");

var todoApi =
builder.AddProject<Projects.TodoApi>("api")
.WithReference(cache)
.WithReference(postgres);

builder.AddNpmApp("frontend", "../TodoApp")
.WithReference(todoApi)
.WithReference("api")
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();

builder.Build().Run();


С помощью директивы #:sdk [email protected] ваш отдельный файл становится полноценным оркестратором для распределённого приложения. Вы определяете инфраструктуру, подключаете зависимости и настраиваете полноценную среду разработки — и всё это без создания файла проекта. Это особенно полезно при создании прототипов архитектур или для быстрого развёртывания тестовой среды.

Переход на полноценный проект
Со временем некоторые скрипты перерастают рамки одного файла. Возможно, требуется разделить код на несколько файлов или, может быть, нужна полноценная поддержка IDE для отладки. Переход проходит без проблем:
dotnet project convert MyUtility.cs

Это создаст правильную структуру проекта, сохраняя все ссылки на пакеты и выбранные SDK. Ваш код переместится в Program.cs, и вы получите файл .csproj, отражающий все использованные вами директивы #:.

Текущие ограничения
В настоящее время проект работает строго в одном файле. Если вам нужно несколько файлов, вам придётся дождаться .NET 11 или преобразовать его в полноценный проект. Конечно, вы по-прежнему можете ссылаться на другие проекты или пакеты, но основной скрипт должен находиться в одном файле.

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

Но с той задачей, для которой он предназначен (обеспечение доступности C# для сценариев написания скриптов), он работает на удивление хорошо. Его можно использовать для скриптов сборки, одноразовой миграции данных, быстрых тестов API или даже для обучения C#, не перегружая новичков структурой проекта.

Итого
Однофайловые приложения ощущаются как C#, наконец-то признавший, что не каждый фрагмент кода должен быть корпоративным приложением. Иногда просто нужно проанализировать файл журнала, иногда нужно быстро протестировать алгоритм, а иногда вы обучаете кого-то программированию и не хотите с первого дня объяснять, что такое файл решения.

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

Источник: https://www.milanjovanovic.tech/blog/exploring-csharp-file-based-apps-in-dotnet-10
👍9
День 2486. #ЗаметкиНаПолях
Удаляем Старые Версии .NET
Со временем на вашем компьютере и серверах, где вы работаете, может накопиться несколько версий среды выполнения и SDK .NET. Хотя наличие нескольких версий часто необходимо для обеспечения совместимости, старые версии, которые больше не нужны, могут занимать ценное дисковое пространство и загромождать систему. Рассмотрим процесс безопасного определения и удаления старых версий .NET.

Зачем?
- Место на диске: каждая версия SDK занимает несколько сотен мегабайт.
- Прозрачность: меньшее количество версий упрощает управление средой разработки.
- Безопасность: старые версии могут содержать известные уязвимости.
- Обслуживание: сохранение только необходимых версий упрощает обновления и устранение неполадок.

Проверка установленных версий
Откройте терминал или командную строку и выполните:
dotnet --list-sdks

Это выдаст все установленные версии SDK. Что-то вроде:
5.0.101 [C:\Program Files\dotnet\sdk]
8.0.100-preview.5.23303.2 [C:\Program Files\dotnet\sdk]
9.0.100-preview.6.24328.19 [C:\Program Files\dotnet\sdk]
10.0.100 [C:\Program Files\dotnet\sdk]

Чтобы проверить версии среды выполнения:
dotnet --list-runtimes


Важно перед удалением
Не удаляйте всё! Перед удалением версий учтите:
- Активные проекты: проверьте, на какие версии SDK ориентированы текущие проекты.
- Требования команды: убедитесь, что не удаляете версии, необходимые для командных проектов.
- Конвейеры CI/CD: убедитесь, что конвейеры сборки не зависят от определённых версий.
- Статус поддержки: сохраните хотя бы одну версию, которая в настоящее время поддерживается.
Вы можете проверить жизненный цикл поддержки .NET на официальной странице Microsoft.

Использование инструмента удаления .NET
Microsoft предоставляет официальный инструмент удаления, который делает процесс безопасным и простым. Скачайте msi-файл с официальной страницы релизов GitHub и запустите установку. После завершения установки инструмент dotnet-core-uninstall станет доступен и в командной строке.

Сначала попробуем команду list, чтобы увидеть, какие версии она может найти и удалить:
dotnet-core-uninstall list

Можно использовать команду пробного запуска с фильтром, чтобы увидеть, на что повлияет выполнение удаления:
dotnet-core-uninstall dry-run --all-below 8.0.0 --hosting-bundle

Если вы готовы, удалите версии:
dotnet-core-uninstall remove --all-below 8.0.0 --hosting-bundle

То же можно сделать для сред выполнения и SDK:
dotnet-core-uninstall remove --all-below 8.0.0 --aspnet-runtime
dotnet-core-uninstall remove --all-below 8.0.0 --runtime
dotnet-core-uninstall remove --all-below 8.0.0 --sdk


Полезные опции
--all-below <version>: Удалить все версии ниже указанной;
--all-but-latest: Оставить только последнюю версию;
--all-previews: Удалить все превью-версии;
--sdk: Точная версия SDK;
--runtime: Точная версия среды выполнения;
--force: Пропустить проверку зависимостей (используйте осторожно!).

Рекомендации
Дополнительные советы по управлению версиями .NET Core:
- Использование global.json: прикрепите определённые проекты к определённым версиям SDK.
- Регулярная очистка: запланируйте ежеквартальный просмотр установленных версий.
- Сохраните одну версию LTS: всегда поддерживайте как минимум одну версию с долгосрочной поддержкой.
- Документируйте зависимости: отслеживайте, какие версии нужны проектам.
- Использование Docker: для старых проектов рассмотрите возможность использования контейнеров Docker вместо локальной установки старых SDK.

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

Источник: https://bartwullems.blogspot.com/2025/11/how-to-uninstall-older-net-core-versions.html
1👍23
День 2487. #ВопросыНаСобеседовании #Архитектура
Оптимизируем Генерацию Отчёта

«Пользователь нажимает кнопку в интерфейсе, чтобы создать отчёт в Excel или PDF. Создание отчёта занимает около пяти минут (время может быть произвольным). Пользователю приходится ждать завершения. Как бы вы оптимизировали этот процесс?»

Первая мысль - производительность: как ускорить создание отчётов? Возможно, оптимизировать SQL, сократить количество преобразований данных, кэшировать часть результатов. Если удастся сократить процесс с пяти минут до одной, это будет большой победой.

Но даже это заставит пользователя ждать. Если браузер зависнет, пропадёт соединение, закроется вкладка, - начинай сначала. Проблема не в производительности, а в дизайне.

Мы часто застреваем на мысли «сделать код быстрее». Это не что-то плохое, оптимизация производительности — ценный навык. Но мы не видим более серьёзной проблемы. Приложение выполняет всю работу синхронно, держа пользователя в заложниках до завершения.

Правильнее было бы спросить не «Как это ускорить?», а «Почему пользователь вообще ждёт?»
Если что-то выполняется минуты (или часы, дни), это не должно блокировать пользователя. Это должно происходить в фоновом режиме, вне основного потока запросов, пока пользователь занимается своими делами.

Однако, не стоит забывать оптимизировать сам код. Запросы к БД, обработка данных и генерация файлов — всё это имеет значение. Возможно, где-то отсутствует индекс, неэффективный цикл или есть более удобная библиотека для создания файлов Excel. Но эти оптимизации — лишь часть решения, а не полная картина.

Как решить проблему долгого ожидания?
Оставим ту же кнопку «Сгенерировать отчёт», но при этом бэкенд примет запрос, сохранит его где-то (например, как запись о задании в БД) и сразу же вернёт управление. В этом суть создания асинхронных API. Затем задание принимается фоновым обработчиком.

В роли обработчика может выступать фоновый сервис, задание Quartz или даже функция AWS Lambda, активируемая сообщением из очереди. Он берёт на себя всю основную работу: извлечение данных, создание файла и его загрузку в хранилище, например, S3 или Azure Blob.

После готовности отчёта обработчик обновит статус задания на «завершено» и уведомит пользователя. Это может быть email со ссылкой для скачивания или сообщение SignalR в режиме реального времени, которое отображается в приложении, со ссылкой на безопасное скачивание сохранённого отчёта.

Теперь пользователю не приходится ждать выполнения длительного HTTP-запроса. Сервер не удерживает открытые соединения в течение нескольких минут. В случае сбоя попытка может быть автоматически повторена. Вы также можете отслеживать ход выполнения или отменить задание при необходимости. А если сто пользователей одновременно запросят отчёты, система может масштабироваться без зависаний.

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

Это интересный вопрос, т.к. он показывает, как люди думают.

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

Другие на мгновение останавливаются и начинают думать о пользовательском опыте, фоновой обработке и отказоустойчивости. Вот тут-то и начинается настоящий разговор: очереди, повторные попытки, уведомления, безопасный обмен файлами и т.д. Существует множество способов превратить этот сценарий в более широкое обсуждение проектирования системы.

Единственно правильного ответа не существует. Но есть большая разница между тем, кто сосредоточен только на коде, и тем, кто может спроектировать масштабируемую систему.

Источник: https://www.milanjovanovic.tech/blog/the-interview-question-that-changed-how-i-think-about-system-design
👍21
День 2488. #SystemDesign101
Как Разработать Хороший API
Хорошо спроектированный API кажется интуитивно понятным, он просто работает. Но за этой простотой кроется набор последовательных принципов проектирования, которые делают API предсказуемым, безопасным и масштабируемым.

Вот что отличает хорошие API от плохих.

1. Идемпотентность
GET, HEAD, PUT и DELETE должны быть идемпотентными. Отправка одного и того же запроса несколько раз даёт один и тот же результат и не содержит непреднамеренных побочных эффектов.

POST и PATCH не являются идемпотентными. Каждый вызов создаёт новый ресурс или каким-либо образом изменяет состояние. Используйте ключи идемпотентности, хранящиеся в Redis или в базе данных. Клиент отправляет один и тот же ключ с повторными попытками, сервер распознает его и возвращает исходный ответ, не обрабатывая его повторно.

2. Версионирование
При изменении контракта API меняйте его версию. Указывать версию можно в URL, строке запроса или в заголовках запроса.

3. Имена ресурсов, основанные на существительных
Ресурсы должны быть существительными, а не глаголами. "/api/products", а не "/api/getProducts".

4. Безопасность
Обеспечьте безопасность каждой конечной точки с помощью надлежащей аутентификации. Bearer-токены (например, JWT) включают заголовок, полезную нагрузку и подпись для проверки запросов. Всегда используйте HTTPS и проверяйте токены при каждом вызове.

5. Пагинация
При возврате больших наборов данных используйте параметры пагинации, например "?limit=10&offset=20", чтобы обеспечить эффективность и согласованность ответов.

См. также «REST vs RESTful. В чём Разница?»

Источник: https://blog.bytebytego.com
👍6