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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2415. #ЗаметкиНаПолях
Реальная Цена Абстракций в .NET. Начало
Мы, разработчики, любим абстракции. Репозитории, сервисы, конвертеры, обёртки. Они делают наш код «чистым», обещают тестируемость и дают нам ощущение гибкости. Некоторые абстракции оправдывают себя, изолируя реальную волатильность и защищая систему от изменений. Другие же незаметно увеличивают сложность, замедляют внедрение и скрывают проблемы производительности за слоями косвенности. Рассмотрим, когда абстракции приносят дивиденды, а когда они становятся техническим долгом.

Когда абстракции окупаются
Лучшие абстракции изолируют реальную волатильность — те части вашей системы, которые вы действительно ожидаете изменить. Пример: обработка платежей. Ваша бизнес-логика не должна напрямую зависеть от API или SDK платёжной системы. Если вы когда-нибудь перейдёте на другую, вы не хотите, чтобы это повлияло на множество мест вашей кодовой базы. Здесь абстракция имеет смысл:
public interface IPaymentProcessor
{
Task ProcessAsync(
Order order, CancellationToken ct);
}

public class StripePaymentProcessor
: IPaymentProcessor
{
public async Task ProcessAsync(
Order order, CancellationToken ct)
{
// Реализация для Stripe
}
}

Теперь бизнес-логика может сфокусироваться на домене:
public class CheckoutService(
IPaymentProcessor processor)
{
public Task CheckoutAsync(
Order order, CancellationToken ct) =>
processor.ProcessAsync(order, ct);
}

Эта абстракция изолирует действительно нестабильную зависимость (платёжного провайдера), сохраняя при этом независимость логики оформления заказа. Когда Stripe поменяет свой API или вы поменяете провайдера, нужно изменить только один класс. Это хорошая абстракция. Она даёт опциональность там, где она действительно нужна.

Когда абстракции становятся техническим долгом
Проблемы возникают, когда мы абстрагируем то, что на самом деле не является изменчивым. В итоге мы оборачиваем стабильные библиотеки или создаём слои, которые не приносят реальной ценности. «Чистый» слой, добавленный вами сегодня, завтра становится обузой для обслуживания.

Большинство команд начинают с чего-то разумного:
public interface IUserRepository
{
Task<IEnumerable<User>> GetAllAsync();
}

Но по мере изменения требований, растёт и интерфейс:
public interface IUserRepository
{
Task<IEnumerable<User>> GetAllAsync();
Task<User?> GetByEmailAsync(string email);
Task<IEnumerable<User>> GetActiveUsersAsync();
Task<IEnumerable<User>> GetUsersByRoleAsync(string role);
Task<IEnumerable<User>> SearchAsync(string keyword, int page, int pageSize);
// ...и т.д.
}

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

Между тем, Entity Framework уже предоставляет всё это через LINQ: строго типизированные запросы, которые напрямую соответствуют SQL. Вместо того, чтобы использовать эту мощь, вы ввели слой косвенности. Паттерн репозитория имел смысл, когда ORM были незрелыми. Сегодня это часто просто формальность.

Частью взросления разработчика является умение распознавать, когда паттерны становятся антипаттернами. Репозитории имеют смысл, когда они инкапсулируют сложную логику запросов или предоставляют унифицированный API для нескольких источников данных. Но вы должны стремиться, чтобы они были сосредоточены на логике предметной области. Как только они разрастаются в мириады методов для каждого возможного запроса, это признак того, что абстракция дала сбой.

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

Источник:
https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
👍22
День 2416. #ЗаметкиНаПолях
Реальная Цена Абстракций в .NET. Продолжение

Начало

Обёртки Сервисов: контекст имеет значение
Хороший пример
При интеграции с внешними API обёртка действительно полезна, поскольку централизует задачи:
public interface IGitHubClient
{
Task<UserDto?> GetUserAsync(string username);
Task<IReadOnlyList<RepoDto>>
GetRepositoriesAsync(string username);
}

public class GitHubClient(HttpClient httpClient)
: IGitHubClient
{
public Task<UserDto?> GetUserAsync(string username) =>
httpClient
.GetFromJsonAsync<UserDto>($"/users/{username}");

public Task<IReadOnlyList<RepoDto>>
GetRepositoriesAsync(string username) =>
httpClient
.GetFromJsonAsync<IReadOnlyList<RepoDto>>(
$"/users/{username}/repos");
}

Эта обёртка изолирует детали API GitHub. При изменении аутентификации или развитии конечных точек вы обновляете одно место. Вашей бизнес-логике не нужно разбираться с HTTP-заголовками, базовыми URL или JSON-сериализацией.

Плохой пример
Проблемы начинаются, когда мы обёртываем наши собственные стабильные сервисы, не добавляя им ценности:
public class UserService(IUserRepository userRepository)
{
// Просто перенаправляем вызовы
public Task<User?> GetByIdAsync(Guid id)
=> userRepository.GetByIdAsync(id);
public Task<IEnumerable<User>> GetAllAsync()
=> userRepository.GetAllAsync();
public Task SaveAsync(User user)
=> userRepository.SaveAsync(user);
}

Этот UserService только добавляет косвенности. Всё, что он делает, — перенаправляет вызовы в IUserRepository. Он не обеспечивает соблюдение бизнес-правил, не добавляет валидацию, не реализует кэширование и не предоставляет никакой реальной функциональности. Это слой, существующий только потому, что «сервисы — это хорошая архитектура».

По мере того, как эти атрофированные обёртки множатся, ваша кодовая база превращается в лабиринт. Разработчики тратят время на перемещение по слоям вместо того, чтобы сосредоточиться на том, где на самом деле находится бизнес-логика.

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

Источник:
https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
👍16👎1
День 2417. #ЗаметкиНаПолях
Реальная Цена Абстракций в .NET. Окончание

Начало
Продолжение

Принимаем более взвешенные решения
Вот как следует понимать, когда абстракции стоят того:

1. Абстрактные политики, а не механизмы
Политики — решения, которые могут измениться: какой платёжный сервис использовать, как обрабатывать кэширование, стратегии повторных попыток для внешних вызовов.
Механизмы — стабильные детали реализации: LINQ в EF Core, конфигурация HttpClient, сериализация JSON.
Абстрагируйте политики, т.к. они обеспечивают гибкость. Не абстрагируйте механизмы — это уже стабильные API, которые редко меняются критически.

2. Дождитесь второй реализации
Если у вас только одна реализация, не поддавайтесь соблазну создать интерфейс. Одна реализация не оправдывает абстрагирование, это преждевременное обобщение, которое добавляет сложности без какой-либо пользы.
// 1: Начинаем с конкретного
public class EmailNotifier
{
public async Task SendAsync(
string to, string subject, string body)
{
// Реализация в SMTP
}
}

// 2: Нужны SMS? Теперь абстрагируемся
public interface INotifier
{
Task SendAsync(string to, string subject, string body);
}

public class EmailNotifier : INotifier { … }
public class SmsNotifier : INotifier { … }

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

3. Реализации внутри, абстракции на границах
Внутри приложения отдавайте предпочтение конкретным типам. Используйте EF напрямую, настраивайте HttpClient как типизированные клиенты, работайте с сущностями домена. Вводите абстракции только там, где система взаимодействует с внешним миром: внешними API, сторонними SDK, инфраструктурными сервисами. Именно там изменения наиболее вероятны, и там абстракции оправдывают себя.

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

Выявив проблемные абстракции, вот как их безопасно удалить:
1. Определите реальных потребителей. Кому на самом деле нужна абстракция?
2. Встройте интерфейс. Замените абстрактные вызовы конкретными реализациями.
3. Удалите обёртку - ненужные косвенные обращения.
4. Упростите вызывающий код. Воспользуйтесь возможностями конкретного API.

Например, замените репозиторий прямым использованием EF:
// До: Скрыто за репозиторием 
var users = await _userRepo
.GetActiveUsersWithRecentOrders();

// После: Прямой запрос
var users = await _context.Users
.Where(u => u.IsActive)
.Where(u => u.Orders.Any(o => o.CreatedAt > DateTime.Now.AddDays(-30)))
.Include(u => u.Orders.Take(5))
.ToListAsync();

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

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

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

Источник: https://www.milanjovanovic.tech/blog/the-real-cost-of-abstractions-in-dotnet
👍15👎2
День 2425. #ЗаметкиНаПолях
Тестирование Текущего Времени с Помощью TimeProvider и FakeTimeProvider
В тестировании сложно использовать то, что зависит от конкретных данных. Представьте себе файловую систему: для корректной работы тестов необходимо убедиться, что файловая система структурирована именно так, как вы ожидаете. Похожая проблема возникает и с датами: если вы создаёте тесты, основанные на текущей дате, при следующем запуске они не пройдут. Нужно найти способ абстрагировать эти функции, чтобы сделать их пригодными для использования в тестах. Сегодня рассмотрим класс TimeProvider, как его использовать и как его имитировать.

Раньше: интерфейс вручную
Раньше самым простым способом абстрагирования управления датами было ручное создание интерфейса или абстрактного класса для доступа к текущей дате:
public interface IDateTimeWrapper
{
DateTime GetCurrentDate();
}

И стандартная его реализация, использующая дату и время в UTC:
public class DateTimeWrapper : IDateTimeWrapper
{
public DateTime GetCurrentDate()
=> DateTime.UtcNow;
}

Или аналогичный подход с абстрактным классом:
public abstract class DateTimeWrapper
{
public virtual DateTime GetCurrentDate() => DateTime.UctNow;
}

Затем нужно просто добавить его экземпляр в движок DI, и всё готово. Единственная проблема - придётся делать это для каждого проекта, над которым вы работаете.

Сейчас: класс TimeProvider
Вместе с .NET 8 команда .NET выпустила абстрактный класс TimeProvider. Помимо предоставления абстракции для локального времени, он предоставляет методы для работы с высокоточными временными метками и часовыми поясами. Важно отметить, что даты возвращаются как DateTimeOffset, а не как экземпляры DateTime. TimeProvider поставляется «из коробки» с консольным приложением .NET:
DateTimeOffset utc = TimeProvider.System.GetUtcNow();
Console.WriteLine(utc);

DateTimeOffset local = TimeProvider.System.GetLocalNow();
Console.WriteLine(local);

А если вам нужно использовать внедрение зависимостей, нужно внедрить его как синглтон:
builder.Services.AddSingleton(TimeProvider.System);

// Использование
public class Vacation(TimeProvider _time)
{
public bool IsVacation
=> _time.GetLocalNow().Month == 8;
}


Тестируем TimeProvider
Мы можем использовать NuGet-пакет Microsoft.Extensions.TimeProvider.Testing, который предоставляет класс FakeTimeProvider, выступающий в качестве заглушки для абстрактного класса TimeProvider. Используя класс FakeTimeProvider, вы можете установить текущее время UTC и местное время, а также настроить другие параметры, предоставляемые TimeProvider:
[Fact]
public void WhenItsAugust_ShouldReturnTrue()
{
// Arrange
var fakeTime = new FakeTimeProvider();
fakeTime.SetUtcNow(
new DateTimeOffset(2025, 8, 14,
22, 24, 12, TimeSpan.Zero));
var sut = new Vacation(fakeTime);

Assert.True(sut.IsVacation);
}

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

Источник: https://www.code4it.dev/csharptips/timeprovider-faketimeprovider/
🔥23👍1
День 2427. #ЗаметкиНаПолях
Особенность ToDictionaryAsync в Entity Framework

В Entity Framework Core ToDictionaryAsync (и, конечно же, его синхронный аналог ToDictionary) извлекает весь объект из БД.

Вот определение ToDictionaryAsync:
public static async Task<Dictionary<TKey, TElement>> ToDictionaryAsync<TSource, TKey, TElement>(
this IQueryable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey>? comparer,
CancellationToken ct = default)
where TKey : notnull
{
Check.NotNull(keySelector);
Check.NotNull(elementSelector);

var d = new Dictionary<TKey, TElement>(comparer);
await foreach (var element in
source.AsAsyncEnumerable()
.WithCancellation(ct)
.ConfigureAwait(false))
{
d.Add(keySelector(element), elementSelector(element));
}

return d;
}


Мы видим, что он принимает лямбды для выбора ключей и значений, а также видим, что этот выбор выполняется на клиенте после получения всего объекта! Поэтому, если у вас есть сущность вроде такой:
public class BlogPost
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Content { get; set; }
public string Author { get; set; }
}

И вы выполняете какой-то такой запрос:
return await dbContext.BlogPosts
.Where(...)
.ToDictionaryAsync(k => k.Author, v => v.Title);

Тогда вы извлекаете из базы данных не только автора и заголовок, но и, описание (Description), и содержание (Content). Решение проблемы описано в этом тикете: используем Select для выбора только нужных полей, так как он выполняется на стороне сервера:
return await dbContext.BlogPosts
.Where(...)
.Select(s => new { Author = s.Author, Title = s.Title })
.ToDictionaryAsync(k => k.Author, v => v.Title);


Источник: https://steven-giesel.com/blogPost/1af57355-7978-40e6-a0f1-3d0ba2c6e1bc/todictionaryasync-retrieves-the-whole-object-from-the-database-in-entity-framework
👍41
День 2429. #ЗаметкиНаПолях
Управляем Временем Жизни DbContext. Начало
DbContext — это сердце EF Core, но его легко использовать неправильно.

Основные правила:
- DbContext представляет собой единицу работы и должен существовать недолго.
- Он не является потокобезопасным; никогда не используйте один экземпляр совместно для параллельных операций.
- В ASP.NET Core стандартным и, как правило, правильным выбором является DbContext с ограниченной (scoped) областью действия для каждого запроса. Для задач вне области действия запроса (фоновые сервисы, одиночные объекты, UI-приложения) используйте фабрику для создания новых контекстов по требованию.
Рассмотрим, как правильно подключать DbContext, когда выбирать каждый вид регистрации и каких ловушек следует избегать.

Виды регистрации
1. AddDbContext (Scoped – по умолчанию, для каждого запроса)
// Program.cs
builder.Services
.AddDbContext<AppDbContext>(o =>
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default")
));

Когда использовать: контроллеры MVC/минимальные API, Razor Pages, хабы SignalR — всё, что находится внутри веб-запроса.
Почему: Вы автоматически получаете отдельный контекст на каждый запрос; EF Core эффективно обрабатывает использование подключений.

2. AddDbContextFactory (Transient - фабрика для контекстов по требованию)
builder.Services
.AddDbContextFactory<AppDbContext>(o =>
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default")
));

Использование:
public sealed class ReportService(
IDbContextFactory<AppDbContext> factory)
{
public async Task<IReadOnlyList<OrderDto>>
GetAsync(CancellationToken ct)
{
await using var db = await
factory.CreateDbContextAsync(ct);
return await db.Orders
.Where(…)
.Select(…)
.ToListAsync(ct);
}
}

Когда использовать:
- Фоновые сервисы (IHostedService, BackgroundService),
- Любые синглтон-сервисы, которым требуется DbContext,
- Десктопные/Blazor-приложения, где требуется свежий контекст для каждой операции.
Почему: Фабрики создают чистые, кратковременные контексты, не полагаясь на внешние области видимости.

3. AddDbContextPool (Scoped с пулингом)
builder.Services
.AddDbContextPool<AppDbContext>(o =>
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default")
));

Когда использовать: Высокопроизводительные API, когда конфигурация контекста стабильна и не сохраняет состояние.
Зачем: Повторное использование экземпляров DbContext из пула для снижения затрат на выделение ресурсов.
Внимание: не сохраняйте состояние запросов в контексте; экземпляры в пуле сбрасываются и используются повторно.

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

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

Источник:
https://thecodeman.net/posts/managing-ef-core-dbcontext-lifetime
1👍36
День 2430. #ЗаметкиНаПолях
Управляем Временем Жизни DbContext. Окончание

Начало

Примеры из реального мира
1. Контроллер / Минимальные API (Scoped)
DI даёт вам scoped-контекст, привязанный к запросу. Одна единица работы, никаких утечек:
app.MapGet("/orders/{id:int}", 
async (int id,
AppDbContext db,
CancellationToken ct) =>
{
var order = await db
.Orders.FindAsync([id], ct);

return order is null
? Results.NotFound()
: Results.Ok(order);
});


2. Фоновые сервисы
Фоновые сервисы не имеют области видимости запроса, вы должны создать область видимости:
public sealed class CleanupService(
IServiceScopeFactory scopes,
ILogger<CleanupService> log)
: BackgroundService
{
protected override async Task
ExecuteAsync(CancellationToken stop)
{
while (!stop.IsCancellationRequested)
{
using var scope = scopes.CreateScope();
var db = scope
.ServiceProvider
.GetRequiredService<AppDbContext>();

// Одна короткоживущая единица работы
// … используем db …
await db.SaveChangesAsync(stop);

await Task.Delay(TimeSpan.FromHours(6), stop);
}
}
}


Либо использовать фабрику:
public sealed class CleanupService(
IDbContextFactory<AppDbContext> factory,
ILogger<CleanupService> log)
: BackgroundService
{
protected override async Task
ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await using var db = await
factory.CreateDbContextAsync(ct);
// … та же логика, что и выше …
}
}
}


3. Синглтон-сервис, которому нужна БД
Синглтоны не должны захватывать DbContext. Создание нового при каждом вызове помогает избежать проблем:
csharp 
public sealed class PricingCache(
IDbContextFactory<AppDbContext> factory,
IMemoryCache cache)
{
public async Task<decimal>
GetPriceAsync(int id, CancellationToken ct)
{
if (cache.TryGetValue(id, out var price))
return price;

await using var db = await
factory.CreateDbContextAsync(ct);
price = await db.Products
.Where(p => p.Id == id)
.Select(p => p.Price)
.FirstAsync(ct);

cache.Set(id, price, TimeSpan.FromMinutes(10));
return price;
}
}


4. Высокопроизводительные APIs (пулинг)
builder.Services
.AddDbContextPool<AppDbContext>(o =>
{
o.UseNpgsql(
builder
.Configuration
.GetConnectionString("Default"));
// Не используйте состояние в опциях
// Избегайте изменяемого состояния
});

Совет: Пулинг улучшает пропускную способность. Он не делает DbContext потокобезопасным. По-прежнему один контекст на запрос.

Распространённые ошибки
1. Совместное использование одного DbContext между потоками
Симптом: "A second operation started on this context before a previous operation was completed." (Вторая операция началась в этом контексте до завершения предыдущей операции).
Решение: один контекст на единицу работы; не выполняйте параллельные запросы в одном и том же контексте.

2. Внедрение DbContext в синглтоны
Решение: внедрение IDbContextFactory или IServiceScopeFactory и создание контекстов по требованию.

3. Долгоживущие контексты
Решение: делайте контексты короткоживущими, иначе раздуваются трекеры изменений и удерживаются соединения.

4. Использование пула с состоянием запросов
Решение: не добавляйте пользовательское состояние к контексту (например, поле CurrentUserId). Контексты в пуле переиспользуются.

5. Попытка «ускорения» путём одновременного выполнения нескольких запросов в одном контексте.
Решение: либо сериализуйте работу, либо создайте несколько контекстов. DbContext не является потокобезопасным.

Итого
Думайте о DbContext как о листке-черновике: вы берёте лист, используете для коротких записей, и выбрасываете его, когда закончите. Не передавайте его по всему офису и не пытайтесь писать на нём двумя ручками одновременно.

Источник: https://thecodeman.net/posts/managing-ef-core-dbcontext-lifetime
👍26
День 2431. #ЗаметкиНаПолях
Экономим Память, Как Профессионалы. 5 Продвинутых Техник. Начало

Если вы уже какое-то время работаете в мире .NET, то знаете, что эта среда выполнения полна скрытых жемчужин. Некоторые из них отточены и дружелюбны, разработаны для того, чтобы новички чувствовали себя в безопасности. Другие — сырые, но мощные инструменты, которым «не обучают в школе», но они дают вам невероятную мощь, если вы знаете, когда и как их использовать. Разберём 5 малоизвестных API, которые могут помочь вам выжать гораздо больше производительности из кода, интенсивно использующего память. Эти функции имеют реальное значение в высокопроизводительных сервисах, конвейерах с малой задержкой и любых приложениях, которые используют много памяти.

1. CollectionsMarshal.AsSpan() для доступа к списку
Обычно, работая с List<T>, вы представляете его себе как удобную и безопасную абстракцию над массивом. Добавляете данные, проходите по списку. Но что, если вам нужен прямой доступ к базовому массиву без копирования?
Стандартный способ:
var numbers = new List<int> { 1, 2, 3, 4, 5 };

// Нужен Span? Придётся делать копию
Span<int> span = numbers.ToArray().AsSpan();
// вариант 2, но всё равно копия
Span<int> span1 = [.. numbers];

// … работа со span’ом

ToArray() создаёт полную копию содержимого списка, тем самым удваивая размер используемой памяти.

Способ лучше:
using System.Runtime.InteropServices;

// Span напрямую над массивом
Span<int> span = CollectionsMarshal.AsSpan(numbers);

Почему: здесь мы работаем непосредственно с внутренним хранилищем списка. Никакого копирования, никаких лишних выделений памяти. Есть одна проблема: если список изменит размер (например, при добавлении нового элемента), span станет недействительным. Именно поэтому этот API находится в System.Runtime.InteropServices.
Когда использовать: при обработке больших буферов или выполнении коротких циклов. Это избавляет от ненужных копий, снижает нагрузку на сборщик мусора и повышает производительность.
Выигрыш: в 2 раза по памяти.

2. CollectionsMarshal.GetValueRefOrNullRef для доступа к словарю
Словари повсюду в приложениях .NET. И чаще всего мы просто делаем что-то вроде этого:
if (dict.TryGetValue("foo", out var value))
{
value++;
dict["foo"] = value;
}

Выглядит безобидно, но мы ищем ключ дважды: в TryGetValue и в индексаторе.

Способ лучше:
using System.Runtime.InteropServices;

ref int valueRef =
ref CollectionsMarshal
.GetValueRefOrNullRef(dict, "foo");

if (!Unsafe.IsNullRef(ref valueRef))
valueRef++;

Почему: С помощью GetValueRefOrNullRef вы получаете ссылку прямо на хранилище словаря. Без повторного поиска, без избыточного вычисления хэша и без лишних записей.
Когда использовать: при работе с большими словарями или циклами, критически важными для производительности (например, с таблицей символов компилятора или кэшем), эти ненужные поиски накапливаются. Этот подход быстрее, но, опять же, слово Unsafe разработчиками языка используется не просто так. Обращайтесь с ref’ами осторожно.
Выигрыш: примерно в 2 раза.

См. также Альтернативный Доступ к Коллекциям в C#13

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

Источник:
https://blog.stackademic.com/stop-wasting-memory-5-advanced-net-tricks-nobody-teaches-you-47376c1339e7
1👍33
День 2432. #ЗаметкиНаПолях
Экономим Память, Как Профессионалы. 5 Продвинутых Техник. Окончание

Начало

3. GC.AllocateUninitializedArray<T> для более быстрой работы с массивами
При создании нового массива в .NET среда выполнения обнуляет его. Обычно это хорошо, предсказуемое поведение, никаких мусорных данных. Но что, если вам не нужно изначальное содержимое, ведь вы всё равно перезапишете каждый элемент? Обнуление — пустая трата времени.
Стандартный способ:
// Автоматически обнуляется
int[] arr = new int[1000];
for (int i = 0; i < arr.Length; i++)
arr[i] = i; // Перезаписываем всё


Способ лучше:
int[] arr = GC.AllocateUninitializedArray<int>(1000);
for (int i = 0; i < arr.Length; i++)
arr[i] = i;

Почему: здесь среда выполнения пропускает очистку памяти. Это ускоряет выделение памяти, особенно для больших массивов.
Когда использовать: требуется дисциплина. Если вы читаете массив перед записью, вы получите мусорные значения. Но в таких сценариях, как сериализация, повторное использование буфера или краткие циклы с вычислениями, это может сэкономить время.
Выигрыш: ~15% для 1000 элементов.

4. ArrayPool<T>.Shared и IMemoryOwner
Если вы многократно выделяете большие массивы в цикле, сборщик мусора вас возненавидит. Именно здесь на помощь приходит ArrayPool<T> — общий пул массивов, который можно арендовать и вернуть.
Стандартный способ:
for (int i = 0; i < 1000; i++)
{
// аллокации каждый раз
var buffer = new byte[1024];
DoSomething(buffer);
}

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

Способ лучше:
using System.Buffers;

for (int i = 0; i < 1000; i++)
{
using IMemoryOwner<byte> owner =
MemoryPool<byte>.Shared.Rent(1024);
var memory = owner.Memory;
DoSomething(memory.Span);
}

Почему: мы арендуем буфер из пула, используем его и возвращаем при уничтожении IMemoryOwner<T>. Никакие выделения памяти не перегружают сборщик мусора.
Когда использовать: особенно актуально в сценариях с высокой пропускной способностью, таких как сетевые серверы, конвейеры или обработка видео. Кроме того, это безопаснее, чем просто арендовать и возвращать массивы, поскольку IMemoryOwner<T> гарантирует корректную очистку.
Выигрыш: более чем в 2 раза быстрее и 1 аллокация вместо 1000.

5. ObjectPool<T> для дорогостоящих объектов
Создание некоторых объектов требует больших затрат. Если вы постоянно создаёте и уничтожаете их, вы тратите не только память, но и ресурсы процессора. Поможет ObjectPool<T>.
Без пулинга:
for (int i = 0; i < 100; i++)
{
var sb = new StringBuilder(1024);
sb.Append("Hello ").Append(i);
Console.WriteLine(sb.ToString());
}

Это создаст 100 экземпляров StringBuilder. Они будут собраны GC, но зачем создавать столько?

Пулинг:
using Microsoft.Extensions.ObjectPool;
var provider = new DefaultObjectPoolProvider();
var pool = provider.CreateStringBuilderPool();

for (int i = 0; i < 100; i++)
{
var sb = pool.Get();
sb.Clear();
sb.Append("Hello ").Append(i);
Console.WriteLine(sb.ToString());
pool.Return(sb);
}

Почему: мы переиспользуем небольшой пул объектов StringBuilder. Выделение памяти значительно сокращается, и мы избегаем постоянного обращения к GC.
Когда использовать: фреймворки логирования, библиотеки сериализации или любые другие рабочие нагрузки, требующие создания множества временных объектов.
Выигрыш: примерно в 4 раза быстрее и в 50 раз меньше по памяти.

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

Источник: https://blog.stackademic.com/stop-wasting-memory-5-advanced-net-tricks-nobody-teaches-you-47376c1339e7
👍47
День 2435. #ЗаметкиНаПолях #AI
Как RAG Позволяет Использовать ИИ для Ваших Данных. Начало
LLM, такие как GhatGPT и Claude, изменили наше взаимодействие с компьютерами. Однако они сталкиваются с фундаментальными ограничениями, которые не позволяют им быть немедленно полезными во многих бизнес-контекстах.

Некоторые ограничения:
- Знания LLM часто заморожены на момент окончания обучения. Если мы спросим GPT-4 о событиях, произошедших после сбора обучающих данных, он не узнает об этом, если не подключится к Интернету для сбора информации.
- LLM не имеют доступа к закрытым данным компании. Когда сотрудники задают вопросы о политике компании, а клиенты интересуются конкретными продуктами, стандартный LLM может давать только общие ответы, основанные на общих закономерностях, которые он, возможно, изучил из общедоступных интернет-данных.
- LLM страдают от галлюцинаций (генерируют правдоподобно звучащую, но неверную информацию). Они могут уверенно приводить выдуманные факты, придумывать цитаты или создавать вымышленные ситуации.
- Даже если у LLM есть соответствующие знания, их ответы могут быть скорее общими, чем конкретными для требуемого контекста.

RAG решает эти проблемы, предоставляя ИИ доступ к конкретным документам и данным.

Что это?
По сути, RAG (Retrieval Augmented Generation) — это метод, объединяющий два различных процесса в одну систему:
- Извлечение релевантной информации из коллекции документов.
- Создание точного ответа на основе этой информации.
Представьте, что вы заходите в библиотеку и задаёте библиотекарю конкретный вопрос о местном налоговом кодексе. Обычный библиотекарь может поделиться общими знаниями о налогах, но библиотекарь, имеющий доступ к конкретным налоговым документам города, может подойти к нужной полке, вытащить соответствующее руководство, прочитать нужный раздел и дать точный ответ, основанный на этих официальных документах. Так работает RAG.

Когда мы спрашиваем стандартную LLM, например, о политике компании в отношении отпусков, она может ответить общей информацией о типичных правилах, с которыми она ознакомилась во время обучения, вроде: «Многие компании предлагают 2 раза по 2 недели оплачиваемого отпуска», потому что это распространённая практика. При использовании RAG система сначала извлекает справочник, находит раздел о политике отпусков, а затем генерирует ответ на основе этого документа. Ответ будет следующим: «Согласно справочнику «…», штатные сотрудники могут планировать отпуска по своему усмотрению, но не более 28 оплачиваемых дней отпуска в год».

Упрощённый принцип работы RAG показан на рисунке ниже.

RAG более полезен, чем стандартный LLM в следующих случаях:
1. Когда сценарий использования включает в себя часто меняющуюся информацию, например, данные о товарных запасах, ценах или новостях.
2. При работе с личной или конфиденциальной информацией, которая не была частью обучающих данных модели, например, внутренней документацией, записями клиентов или конфиденциальными исследованиями.
3. Когда точность критически важна, а галлюцинации недопустимы, например, в юридических, медицинских или финансовых приложениях.
4. Когда важно предоставить ссылки или доказать источник информации. Система может указать на конкретные документы и отрывки, обеспечивая прозрачность и контролируемость, которые невозможны при использовании стандартных ответов LLM.
5. Когда приложению необходимо обрабатывать большие коллекции документов, которые было бы непрактично включать в каждое сообщение.

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

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

Источник:
https://substack.com/home/post/p-174052561
👍8
День 2436. #ЗаметкиНаПолях #AI
Как RAG Позволяет Использовать ИИ для Ваших Данных. Продолжение

Начало

Как работает RAG
Работа проходит в 2 этапа:
1. Подготовка документа - один раз при настройке системы или позже, при добавлении в систему новых документов или источников информации.
2. Обработка запроса - в режиме реального времени каждый раз, когда пользователь задаёт вопрос.
Этот двухэтапный подход эффективен благодаря разделению задач между этапом подготовки документа, требующим больших вычислительных мощностей, и этапом выполнения запроса, чувствительным к задержкам.

1. Подготовка (индексация)
Эта основополагающая работа выполняется до поступления пользовательских запросов и включает несколько важных этапов. См. схему 1 ниже.

1) Сбор и обработка документов. Каждый документ (PDF, Word, веб-страница или запись БД) должен быть преобразован в обычный текст. Процесс обрабатывает различные форматы и гарантирует чёткое отделение фактического содержимого от форматирования и метаданных.

2) Разделение текста на мелкие фрагменты. Документы обычно слишком длинные для обработки как единое целое. Размер фрагментов имеет значение: слишком маленький — они теряют контекст, слишком большой — они становятся менее точными. Большинство систем используют фрагменты по 500–1000 слов, часто с некоторым перекрытием между последовательными фрагментами для сохранения контекста.

3) Фрагменты преобразуются в числовые представления – эмбеддинги - список чисел, отражающих его семантическое значение, например, [0,23, -0,45, 0,67, …] с сотнями или тысячами измерений. Числа кодируют смысл текста так, что это позволяет проводить математические сравнения. Схожие понятия порождают схожие числовые шаблоны, что позволяет системе находить связанный контент даже при использовании разных слов.

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

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

1) Вопрос пользователя попадает в систему и проходит тот же с использованием той же модели эмбеддинга, которая использовалась для обработки документов.

2) Система ищет в векторной БД наиболее похожие фрагменты документов. Этот поиск быстр, т.к. использует математические операции, а не сравнение текстов. Обычно система извлекает от 3 до 10 наиболее релевантных фрагментов.

3) Извлечённые фрагменты подготавливаются для языковой модели. Система объединяет их в контекст, часто ранжируя по релевантности и фильтруя на основе метаданных или бизнес-правил.

4) Языковая модель получает исходный вопрос пользователя и извлеченный контекст. Промпт может содержать:
- Предоставленные документы контекста.
- Конкретный вопрос пользователя.
- Инструкции по ответу на основе предоставленного контекста.
- Рекомендации по обработке информации, отсутствующей в контексте.

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

6) Ответ часто проходит постобработку: добавление ссылок на исходные документы, форматирование ответа для лучшей читаемости или проверку на соответствие заданному вопросу. Некоторые системы также логируют запрос, извлечённые документы и ответ для аналитики и улучшения.

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

Источник:
https://substack.com/home/post/p-174052561
👍11
День 2437. #ЗаметкиНаПолях #AI
Как RAG Позволяет Использовать ИИ для Ваших Данных. Продолжение

Что такое RAG
Как работает RAG

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

Представьте себе разнообразие способов задать вопрос о проблеме с компьютером: «ноутбук не запускается», «компьютер не загружается», «система не включается» или «ПК не работает». В этих фразах практически нет общих слов, но все они описывают одну и ту же проблему. Система, основанная на ключевых словах, воспримет их как совершенно разные запросы и пропустит руководства по устранению неполадок, использующие другую терминологию.

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

Процесс преобразования текста в числа:
- Модель эмбеддинга считывает текст и выводит список чисел, обычно сотни или тысячи.
- Эти числа позиционируют текст как точку в многомерном пространстве.
- Точка подобна координатам на карте, но вместо двух измерений (широта и долгота) эмбеддинги используют сотни измерений для передачи множества нюансов смысла.
См. схему ниже.

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

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

Интересно, что мы не до конца понимаем, что представляет собой каждое измерение. Когда модель встраивания выдаёт 768 чисел для фрагмента текста, мы не можем просто сказать, что измерение 1 представляет «формальность», а измерение 547 — «техническую сложность». Эти измерения возникают естественным образом в процессе обучения, когда модель определяет закономерности, которые ей необходимо отслеживать для эффективного понимания языка. Некоторые измерения могут слабо коррелировать с человеческими понятиями, такими как тональность или тема, а многие отражают абстрактные закономерности, которые не соответствуют ни одному понятию, для которого у нас есть слова.

Модели эмбеддинга и большие языковые модели (LLM) служат разным целям в системе RAG. Модель эмбеддинга небольшая, специализирована для одной задачи: преобразования текста в числа. Модели LLM массивные, предназначены для понимания и генерации текста, похожего на человеческий. В то же время они дорогие. Поэтому системы RAG используют две отдельные модели. Модель эмбеддинга эффективно преобразует все документы и запросы в векторы, обеспечивая быстрый поиск по семантическому сходству. Затем LLM берёт найденные релевантные документы и генерирует интеллектуальные, контекстные ответы.

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

Источник:
https://substack.com/home/post/p-174052561
👍13
День 2438. #ЗаметкиНаПолях #AI
Как RAG Позволяет Использовать ИИ для Ваших Данных. Окончание

Что такое RAG
Как работает RAG
Эмбеддинги

Создание RAG-системы

1. Понимание требований.
Разрабатывается ли система для внутренних сотрудников, для которых важна точность и надежность? Или для внешних клиентов, для которых важнее скорость и взаимодействие с UI?

2. Структура документов.
Масштаб имеет значение. Различные объёмы требуют разных стратегий хранения и поиска. Возможные типы контента определяют конвейеры загрузки и предварительной обработки.

3. Шаблоны запросов.
Являются ли большинство запросов простыми поисковыми запросами, например, «Какова наша политика в отношении отпусков?», или требуют сложных рассуждений, например, «Сравните стратегии продаж в III и IV кварталах»? Ожидают ли пользователи точных цитат или просто ответов в разговорном формате? Это определяет, насколько сложной должна быть система.

4. Технологический стек.
Вот некоторые популярные инструменты и технологии.
- LLM. С закрытым исходным кодом (GPT-4 от OpenAI, Claude от Anthropic, Gemini от Google), которые легко внедряются и обеспечивают высокую производительность, но привязаны к поставщику и имеют проблемы конфиденциальности данных. Модели с открытым исходным кодом (Llama 3, Mistral, или специализированные BioBERT для медицины и FinBERT для финансов), обеспечивают больший контроль и гибкость, но требуют инфраструктуры графических процессоров и наличия собственных специалистов для масштабирования.

- Модель эмбеддинга. Распространенные варианты: text-embedding-3 от OpenAI или embed-v3 от Cohere или бесплатные альтернативы с открытым исходным кодом. Специализированные модели, такие как E5 или Instructor, могут дополнительно повысить точность, специфичную для предметной области. Модели LLM и эмбеддинга не обязательно должны предоставляться одним и тем же поставщиком.

- Векторная БД, в которой хранятся и ищутся эмбеддинги: Pinecone, Weaviate Cloud или Qdrant Cloud, отлично подходят для быстрого начала работы и плавного масштабирования, хотя и стоят дороже. Решения с собственным хостингом, такие как ChromaDB, Milvus, Elasticsearch с поиском векторов или расширение PostgreSQL pgvector, обеспечивают больший контроль и могут быть дешевле в долгосрочной перспективе, но требуют инвестиций в DevOps. Правильный выбор зависит от объёма данных (сотни тысяч против миллиардов векторов), нагрузки запросов (десятки против десятков тысяч запросов в секунду) и бюджета.

- Фреймворк для оркестровки. Немногие команды разрабатывают всё с нуля. LangChain — самый популярный фреймворк с широкой экосистемой и абстракциями практически для каждого компонента, хотя в простых случаях он может показаться слишком громоздким. LlamaIndex разработан специально для приложений RAG с большим объёмом документов и предлагает чистый приём данных и конвейеры запросов. Haystack ориентирован на производство и обладает мощной поддержкой сложных рабочих процессов.

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

Источник:
https://substack.com/home/post/p-174052561
👍4
День 2439. #ЗаметкиНаПолях
Используем insteadOf в Git для Замены HTTPS-адресов на SSH
При работе с Git-репозиториями вы часто сталкиваетесь с URL-адресами в формате HTTPS, особенно при клонировании из GitHub, GitLab или других хостинг-провайдеров. Однако, если вы предпочитаете использовать SSH для аутентификации (что часто удобнее с аутентификацией по ключам), ручное изменение URL-адресов может быть утомительным. Кроме того, это может быть ещё сложнее при работе с субмодулями.

Конфигурационная опция Git insteadOf предлагает элегантное решение, автоматически переписывая URL-адреса «на лету»:
git config --global url."git@github.com:".insteadOf "https://github.com/"
git config --global url."git@gitlab.com:".insteadOf "https://gitlab.com/"
git config --global url."git@bitbucket.org:".insteadOf "https://bitbucket.org/"


Вы также можете настроить более конкретную проверку и выбрать только определённые репозитории или сервисы. Например, если вы хотите заменить HTTPS-адреса некоторых репозиториев, вы можете сделать следующее:
git config --global url."git@github.com:username/".insteadOf "https://github.com/username/"


После настройки Git будет автоматически переписывать URL:
# Следующая команда
git clone https://github.com/user/repo.git

# Станет эквивалентной
git clone git@github.com:user/repo.git


Перезапись происходит прозрачно. Вы по-прежнему можете использовать HTTPS-адреса в командах, документации или при копировании из веб-интерфейсов.

Ещё один вариант использования insteadOf — добавление аутентификации к URL. Например, если вы хотите использовать определённого пользователя для всех запросов Git, вы можете сделать следующее:
git config --global url."https://<token>@github.com/".insteadOf "https://github.com/"


Источник: https://www.meziantou.net/using-git-insteadof-to-automatically-replace-https-urls-with-ssh.htm
👍16
День 2441. #ЗаметкиНаПолях
Используем Хранимые Процедуры в EF Core. Начало

Необходимость в хранимых процедурах в БД может возникнуть по разным причинам:
- сложный отчёт, объединяющий несколько таблиц с помощью агрегаций и оконных функций, когда LINQ-запрос генерирует неоптимальный SQL;
- нужно обновить таблицу, используя правильные блокировки, чтобы предотвратить состояние гонки.

В разных источниках об использовании хранимых процедур в EF Core можно встретить противоречивые советы: от «избегать использования чистого SQL любой ценой» до «полностью отказаться от EF и использовать ADO.NET». Ни то, ни другое не кажется правильным. EF Core отлично работает с функциями и процедурами БД.

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

Когда стоит использовать чистый SQL?
В большинстве случаев LINQ вполне подходит. EF Core преобразует ваш код C# в качественный SQL, а вы получаете типобезопасность и поддержку рефакторинга. Но есть исключения:
1. Нужна производительность, которую невозможно получить от LINQ. Сложные агрегации с несколькими соединениями, оконными функциями или запросы к отчётам часто выполняются быстрее, если написаны непосредственно на SQL. Вы можете протестировать и настроить запрос в инструменте работы с БД, прежде чем добавлять его в код.
2. Специфичные для БД функции. В PostgreSQL есть полнотекстовый поиск запросы к JSON и общие табличные выражения (CTE), которые не всегда имеют понятные эквиваленты в LINQ.
3. Есть существующая логика в БД (хранимые процедуры и функции, например, из унаследованной системы). Их прямой вызов лучше, чем переписывание всего на C#.
4. Нужны атомарные операции с корректной блокировкой. Хранимая процедура, которая координирует несколько обновлений с помощью блокировок FOR UPDATE, проще и безопаснее, чем пытаться управлять этим из кода приложения.
5. Нужно сократить количество запросов. Один вызов функции, агрегирующий данные из пяти таблиц, эффективнее пяти отдельных LINQ-запросов.

Рассмотрим, как это сделать.

1. Простая скалярная функция
Вот простая функция, которая показывает, сколько билетов осталось:
CREATE OR REPLACE FUNCTION 
tt.tickets_left(ticket_type_id uuid)
RETURNS numeric LANGUAGE sql
AS $$
SELECT tt.available_quantity
FROM ticketing.ticket_types tt
WHERE tt.id = ticket_type_id
$$;

Ничего необычного, просто запрос, обёрнутый в функцию. Вызвать её в EF Core просто:
var result = await dbContext.Database.SqlQuery<int>(
$"""
SELECT tt.tickets_left({ticketTypeId}) AS "Value"
""")
.FirstAsync();

Обратите внимание на алиас AS "Value". Когда EF Core сопоставляется с примитивным типом, он ожидает свойство с именем Value. Кавычки сохраняют точный регистр (PostgreSQL по умолчанию преобразует идентификаторы без кавычек в нижний регистр).

Синтаксис интерполированной строки ($"{ticketTypeId}") может показаться опасным и подверженным SQL-инъекциям, но EF Core автоматически преобразует его в параметризованный запрос. Это всего лишь удобный способ написания параметризованных запросов. Причина в том, что мы передаём методу SqlQuery не строку, а FormattableString. Это специальный тип, который сохраняет формат и аргументы отдельно, позволяя EF Core обрабатывать параметры.

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

Источник:
https://www.milanjovanovic.tech/blog/using-stored-procedures-and-functions-with-ef-core-and-postgresql
👍16
День 2442. #ЗаметкиНаПолях
Используем Хранимые Процедуры в EF Core. Продолжение

Начало

2. Функция, возвращающая таблицу
Функции могут возвращать целые наборы результатов:
CREATE OR REPLACE FUNCTION 
tt.customer_order_summary(customer_id uuid)
RETURNS TABLE (
order_id uuid,
created_at timestamptz,
total numeric,
currency text,
qty numeric
)
LANGUAGE sql
AS $$
SELECT
o.id, o.created_at, o.total, o.currency,
COALESCE(SUM(oi.qty), 0) AS qty
FROM tt.orders o
LEFT JOIN tt.order_items oi ON oi.order_id = o.id
WHERE o.customer_id = customer_id
GROUP BY o.id, o.created_at, o.total, o.currency
ORDER BY o.created_at DESC
$$;

Эта функция объединяет заказы с их товарами, суммирует количество и возвращает несколько строк. Вы можете написать это на LINQ, но SQL понятнее, и вы можете протестировать его непосредственно в инструменте для работы с БД. Чтобы использовать её из C#, создайте DTO, соответствующий выходным данным функции:
public class OrderSummaryDto
{
public Guid OrderId { get; set; }
public DateTime CreatedAt { get; set; }
public decimal Total { get; set; }
public string Currency { get; set; }
public int Quantity { get; set; }
}

Затем запросите функцию, как и любую другую таблицу:
var orders = 
await dbContext.Database
.SqlQuery<OrderSummaryDto>(
$"""
SELECT order_id AS OrderId, created_at AS CreatedAt, total AS Total, currency AS Currency, qty AS Quantity
FROM tt.customer_order_summary({customerId})
""")
.ToListAsync();

Ключевым моментом является сопоставление имён столбцов со свойствами DTO с помощью алиасов. EF Core обрабатывает всё остальное автоматически.

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

Понимание функций и процедур PostgreSQL
PostgreSQL различает функции и процедуры:
- Функции предназначены для возврата значений. Они могут возвращать скалярные значения, таблицы или даже сложные JSON-объекты. Они вызываются внутри транзакции с помощью SELECT и могут использоваться в запросах, предложениях WHERE, объединениях и других контекстах запросов.
- Процедуры предназначены для побочных эффектов. Они не возвращают значения напрямую, но могут изменять данные и иметь OUT-параметры. Вызываются с помощью CALL и идеально подходят для сложных операций, требующих явного управления транзакциями или выполнения нескольких связанных обновлений.

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

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

Источник:
https://www.milanjovanovic.tech/blog/using-stored-procedures-and-functions-with-ef-core-and-postgresql
👍8
День 2443. #ЗаметкиНаПолях
Используем Хранимые Процедуры в EF Core. Окончание

Начало
Продолжение

3. Хранимая процедура с валидацией
Допустим, вам нужно изменить количество доступных билетов, но вы хотите предотвратить состояния гонки и проверить операцию:
CREATE OR REPLACE PROCEDURE 
tt.adjust_available_quantity(
ticket_type_id uuid,
delta numeric
)
LANGUAGE plpgsql AS $$
DECLARE
v_qty numeric;
v_avail numeric;
v_new_avail numeric;
BEGIN
SELECT quantity, available_quantity
INTO v_qty, v_avail
FROM tt.ticket_types
WHERE id = ticket_type_id
FOR UPDATE;

IF NOT FOUND THEN
RAISE EXCEPTION 'Тип билета % не найден', ticket_type_id;
END IF;

v_new_avail := v_avail + delta;

IF v_new_avail < 0 THEN
RAISE EXCEPTION 'Нельзя уменьшить < 0';
END IF;

IF v_new_avail > v_qty THEN
RAISE EXCEPTION 'Нельзя превышать общее количество';
END IF;

UPDATE tt.ticket_types
SET available = v_new_avail
WHERE id = ticket_type_id;
END;
$$;

Процедура делает несколько важных вещей:
- «Запирает» строку для обновления (FOR UPDATE), чтобы другая транзакция не могла её обновить, пока не завершится хранимая процедура;
- Проверяет бизнес-правила перед внесением изменений;
- Выводит понятные сообщения об ошибках при возникновении проблем;
- Сохраняет всё атомарно в одном запросе к БД.
Всё это можно было бы реализовать на C# с ручным управлением транзакциями и явной блокировкой, но это сложнее и подвержено ошибкам. Позвольте БД делать то, в чём она хороша. Вот как это можно вызвать из EF Core:
try
{
await dbContext.Database.ExecuteSqlAsync(
$"""
CALL tt.adjust_available_quantity({ticketTypeId},{quantity})
""");
}
catch (Exception e)
{
// обработка ошибки
}

Процедура не возвращает значение, но, если она выбрасывает исключение (с помощью RAISE EXCEPTION), PostgreSQL передаст его в ваш код C#. Вы можете перехватить его и вернуть корректный ответ об ошибке.

Представления (view)
Представления БД подобны функциям без параметров. Это сохранённые запросы, к которым можно обращаться по имени. Можно выполнять запросы к ним, используя SqlQuery<T>, как к функциям:
var results = await dbContext.Database
.SqlQuery<ActiveCustomerDto>(
$"SELECT * FROM tt.active_customers")
.ToListAsync();

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

Итого
EF Core не заставляет вас выбирать между LINQ и чистым SQL. Вы можете использовать и то, и другое. Используйте функции, когда нужно вернуть данные, процедуры, когда нужно изменить данные со сложной логикой, и чистые SQL-запросы, когда LINQ не может эффективно удовлетворить ваши требования. Сочетание удобства EF Core и мощности БД даёт гибкость в выборе подходящего инструмента для каждого сценария.

Источник: https://www.milanjovanovic.tech/blog/using-stored-procedures-and-functions-with-ef-core-and-postgresql
👍8
День 2448. #ЗаметкиНаПолях
Использование Токенов Отмены

Сегодня посмотрим, почему стоит использовать токены отмены в вашем API. Дело не только в вашем коде.

Представьте, что у нас есть длительный SQL-запрос, например:
-- MSSQL
SELECT COUNT_BIG(*)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
CROSS JOIN sys.all_objects c
CROSS JOIN sys.all_objects d
CROSS JOIN sys.all_objects e;

Конечно, это всего лишь «бесполезный» демонстрационный код, но идея в том, что он выполняется долго и может значительно нагружать процессор, память и ввод-вывод. Если это часть вашего (REST) API и HTTP-запрос отменяется, SQL-запрос всё равно продолжит выполняться.

Вы можете легко проверить это с помощью:
SELECT * FROM sys.dm_exec_requests WHERE status = 'running';

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

Теперь, если мы предоставим токен отмены:
using var cts = 
new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
await dbContext.Database.ExecuteSqlAsync(
$"""
SELECT COUNT_BIG(*)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b
CROSS JOIN sys.all_objects c
CROSS JOIN sys.all_objects d
CROSS JOIN sys.all_objects e;
""", cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Запрос отменён!");
}

Теперь, если запустить тот же запрос к sys.dm_exec_requests, вы увидите, что после отмены, сервер также прерывает и SQL-запрос.

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

В этом примере это был SQL-сервер, который реагировал на отмену. Но если вы также передадите токен отмены в HttpClient, это прервёт запрос, и другая сторона может отреагировать таким же образом. См. Отмена Операции при Отмене HTTP Запроса.

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

Всегда ли нужно предоставлять токен отмены?
Если вы видите в вопросе слова «всегда» или «никогда», ответ, скорее всего, «нет». Конечно, общее правило — добавлять токен отмены к вызовам, которые его поддерживают, но давайте посмотрим на следующий код:
var user = new User { Name = dto.Name };
db.Users.Add(user);
await _db.SaveChangesAsync(ct);

var address =
new Address { UserId = user.Id, City = dto.City };
db.Addresses.Add(address);
await _db.SaveChangesAsync(ct);

var role =
new Role { UserId = user.Id, Name = "Member" };
db.Roles.Add(role);
await _db.SaveChangesAsync(ct);

Представьте, что пользователь отменяет запрос, во время сохранения адреса. Пользователь добавляется, а адрес и роль — нет. Теперь вы находитесь в несогласованном состоянии. Конечно, этот пример надуманный. Простое решение — вызвать SaveChangesAsync только один раз в конце или использовать транзакцию, которая фиксируется в конце один раз. Но иногда у вас есть сторонний код, который также хранит информацию где-то, где вы не можете её контролировать. Поэтому нужно убедиться, что вы не столкнётесь с несогласованными состояниями из-за того, что кто-то прервал запрос!

Источник: https://steven-giesel.com/blogPost/080baaef-27d4-4d98-b0a8-9c3ab96c335e/use-cancellationtokens
👍19
День 2455. #ЗаметкиНаПолях
Когда Type.FullName Возвращает null
Немного бесполезной информации вам в ленту. Казалось бы, имя типа в .NET всегда должно быть известно. Однако сигнатура метода Type.FullName такая:
public abstract string? FullName { get; }


Такое поведение может показаться неожиданным, но существуют определённые сценарии, в которых среда выполнения .NET не может сгенерировать корректное полное имя для типа. Вот два основных случая, когда Type.FullName возвращает null.

1. Обобщённые типы с открытыми параметрами типа
При создании обобщённого типа, содержащего несвязанные обобщённые параметры:
var list = typeof(IList<>);
var dict = typeof(IDictionary<,>);
var listOfDictionaries = list.MakeGenericType(dict);
// IList<IDictionary<,>>
Assert.Null(listOfDictionaries.FullName);


2. Указатели на функции
Указатели на функции, введённые в C#9, также имеют null в FullName:
var functionPointerType = typeof(delegate*<int, void>);
Assert.Null(functionPointerType.FullName);


Источник: https://www.meziantou.net/understanding-when-type-fullname-returns-null-in-dotnet.htm
👍14
День 2460. #ЗаметкиНаПолях
Встраивание и Структуры в C#

Сегодня рассмотрим встраивание структур в C#. И как оно может оптимизировать производительность несколькими интересными способами.

Встраивание
Встраивание — это оптимизация компилятора, которая заменяет вызов метода его телом. Например:
public int Add(int a, int b) => a + b;

public int CalculateSum(int x, int y)
=> Add(x, y);

Компилятор может оптимизировать его до:
public int CalculateSum(int x, int y)
=> x + y;


Очевидное преимущество здесь в том, что мы избегаем накладных расходов на вызов метода, но это также может увеличить размер кода и негативно сказаться на производительности (поскольку мы копируем тело метода во все места его вызова). Существует атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)], который можно использовать, чтобы подсказать компилятору, что метод следует встраивать, даже если по умолчанию он этого не делает. Это всего лишь подсказка, и JIT всё равно может её проигнорировать. Мы также можем использовать [MethodImpl(MethodImplOptions.NoInlining)], чтобы предотвратить встраивание метода (подсказать JIT, что встраивать его не следует).

Структуры
Неотъемлемой частью структур является то, что они, как правило, передаются по значению. Это означает, что при передаче структуры методу создаётся её копия. Т.е.:
public struct Point
{
public int X;
public int Y;
}
public void MovePoint(Point p)
{
p.X += 10;
p.Y += 10;
}
Point myPoint = new Point { X = 0, Y = 0 };
MovePoint(myPoint);
// myPoint всё ещё { X = 0, Y = 0 }

В идеале структуры лучше сохранять неизменяемыми именно по этой причине. А также делать небольшими, чтобы избежать накладных расходов на копирование. И вот в чём «прелесть»: встраивая функцию, мы «стираем» необходимость копирования структуры в новый стековый фрейм. Т.е. встраивание может фактически удешевить передачу структур.

Посмотрите вот этот пример на sharplab.io. Вам не нужно понимать JIT-код ASM. Но сама разница в объёме кода между InlineVsNonInlineBenchmark.NonInline() и InlineVsNonInlineBenchmark.Inline() показывает, что Inline скорее всего будет работать быстрее. По сути, NonInline приходится много копировать (инструкции vmovdqu и vmovq), в то время как Inline просто считывает свойство, добавляет что-то и возвращает результат.

PS: на самом деле JIT совершает множество гораздо более хитрых оптимизаций вашего кода. На последнем DotNext об этом был хороший доклад Дмитрия Егорова "JIT не волшебство: как он работает и как не мешать". У кого есть доступ, обязательно посмотрите. Остальные - подождите выхода на Youtube.

Источник: https://steven-giesel.com/blogPost/e89d7156-f3fd-4152-b78a-cb908bc43226/inlining-and-structs-in-c
👍3