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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2674. #МоиИнструменты #PG
Инструменты Оптимизации Запросов в PostgreSQL. Часть 10

10. pg_qualstats (Статистика Предикатов Запросов)
Что даёт: статистику, какие условия WHERE больше всего выиграют от индексов.

Зачем нужен
Создание индексов — это гадание на кофейной гуще без данных. Какие столбцы на самом деле запрашиваются чаще всего? Какие предикаты больше всего нагружают базу? pg_qualstats отслеживает использование условий WHERE во всех запросах, точно показывая, какие индексы окажут максимальное влияние.

Установка
1. Устанавливаем расширение
CREATE EXTENSION pg_qualstats;

2. Добавляем в postgresql.conf
shared_preload_libraries = 'pg_stat_statements,pg_qualstats'
pg_qualstats.enabled = on
pg_qualstats.track_constants = on

3. Перезапускаем PostgreSQL.

Использование
После работы производственной базы (часы/дни), выполняем запрос на поиск наиболее используемых предикатов:
SELECT 
qualid,
queryid,
userid,
dbid,
lrelid::regclass AS table_name,
lattnum AS column_number,
opno::regoperator AS operator,
eval_type,
count AS execution_count
FROM pg_qualstats
ORDER BY count DESC
LIMIT 20;

Пример вывода:
table_name | column   | operator | execution_count
orders | customer_id | = | 450,234
orders | order_date | >= | 234,567
orders | status | = | 123,456
customers | email | = | 89,234

Т.е. "customer_id = ?" использовалось 450 тыс раз. Это явный кандидат на индекс.

Предложения индексов
SELECT 
v.relname,
v.attnames,
sum(v.execution_count) as execution_count,
sum(v.nbfiltered) as rows_filtered
FROM (
SELECT
lrelid::regclass AS relname,
array_agg(DISTINCT attname) AS attnames,
count AS execution_count,
nbfiltered
FROM pg_qualstats
JOIN pg_attribute ON (attrelid = lrelid AND attnum = lattnum)
WHERE nbfiltered > 100 -- Только избирательные предикаты
GROUP BY lrelid, lattnum, count, nbfiltered
) v
GROUP BY v.relname, v.attnames
ORDER BY execution_count DESC;

Вывод: предложения в порядке значимости
1. orders(customer_id) – 450 тыс раз, фильтрует 99.9% строк
2. orders(order_date) - 234 тыс раз, фильтрует 95% строк
3. orders(status) - 123 тыс раз, фильтрует 80% строк

Когда использовать
- Большая БД с неясными потребностями в индексировании;
- Необходимы обоснованные решения по индексам;
- Кампания по оптимизации индексов.

Когда отказаться
- Небольшая БД (достаточно ручного анализа);
- Индексы уже хорошо оптимизированы;
- Не используете PostgreSQL.

Скрытая функция
Выявление неиспользуемых индексов (противоположный вариант использования). Объедините pg_qualstats с pg_stat_user_indexes, чтобы найти индексы, которые можно удалить.
SELECT 
schemaname,
tablename,
indexname,
idx_scan AS index_scans,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0 -- не используется
AND indexrelid NOT IN (
-- Исключаем индексы из pg_qualstats
SELECT DISTINCT lrelid
FROM pg_qualstats
WHERE count > 0
)
ORDER BY pg_relation_size(indexrelid) DESC;

Вывод: индексы, занимающие место, но не используемые, которые можно удалить.

С осторожностью
Настройки по умолчанию могут вызывать накладные расходы в размере 5-10% при очень высокой частоте запросов в секунду. Решение: снижение накладных расходов путём выборки. В postgresql.conf:
# Отслеживаем 10% запросов
pg_qualstats.sample_rate = 0.1
# Исключаем пользователей/базы
pg_qualstats.exclude_users = 'readonly_user,report_user'

Проверка нагрузки
SELECT 
pg_stat_statements.query,
pg_qualstats.count AS qualstats_calls,
pg_stat_statements.calls AS total_calls,
(pg_qualstats.count::float / pg_stat_statements.calls) AS tracking_ratio
FROM pg_qualstats
JOIN pg_stat_statements USING (queryid);

Если tracking_ratio > 0.1 на частых запросах, уменьшите sample_rate.

Источник: https://medium.com/@reliabledataengineering/15-sql-optimization-tools-that-make-queries-10x-faster-8629ac451d97
👍6
День 2675. #ЗаметкиНаПолях
Реализуем Немедленный Отзыв Токенов в .NET 10. Начало

Представьте себе этот кошмарный сценарий. У клиента банка украли телефон, где ваше мобильное приложение авторизовано, предоставляя вору полный доступ к счетам клиента. В службу поддержки поступает звонок. Каждая секунда на счету. Какова скорость вашей реакции для отмены активной сессии и обеспечения безопасности средств клиента?
Если вы полагаетесь на стандартные автономные JWT-токены, честный ответ может быть «до часа», в зависимости от срока действия токена. Этого недостаточно. Рассмотрим, как референтные токены предоставляют вам кнопку экстренной остановки именно в таких ситуациях, и как это реализовать в Duende IdentityServer в .NET 10.

Проблема с автономными JWT
Автономные JWT — это основной инструмент современной авторизации. Они содержат все необходимые API данные для принятия решений о доступе внутри себя. Никаких обращений к БД или сетевых вызовов к поставщику идентификации. API проверяет подпись, проверяет срок действия, и вы получаете доступ. Просто и быстро.

Но это палка о двух концах. После выдачи JWT поставщику идентификации больше нечего о нём сказать. Токен действителен в течение срока действия, обычно от 5 до 60 минут. Если устройство украдено, учётная запись пользователя скомпрометирована или обнаружена угроза, вы не можете отозвать этот токен. Вы вынуждены ждать, пока он истечёт.

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

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

Поскольку данные токена хранятся на сервере, вы можете удалить их в любое время. Отзыв происходит немедленно. В следующий раз, когда API вызовет конечную точку интроспекции, он получит в ответ "active": false, и доступ будет запрещён.

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

Когда использовать референтные токены
Референтные токены не являются универсальной заменой JWT. Они проявляют свои преимущества в определённых сценариях:
- Немедленное аннулирование является жёстким требованием (банковская сфера, здравоохранение, системы, ориентированные на соответствие нормативным требованиям);
- Внутреннее взаимодействие между сервисами, где время отклика при интроспекции незначительно;
- Операции с высоким риском, где преимущества в безопасности перевешивают затраты на производительность.

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

Далее рассмотрим реализацию.

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

Источник:
https://duendesoftware.com/blog/20260428-the-emergency-stop-button-implementing-immediate-token-revocation-in-dotnet-10
👍6
День 2676. #ЗаметкиНаПолях
Реализуем Немедленный Отзыв Токенов в .NET 10. Окончание

Начало

Настройка токенов
Переключение клиента на использование референтных токенов - одна строка конфигурации. При определении клиента в IdentityServer установите AccessTokenType:
new Client
{
ClientId = "banking_app",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,

// Вот эта строка
AccessTokenType = AccessTokenType.Reference,

AllowOfflineAccess = true,
RedirectUris = { "https://banking.example.com/signin-oidc" },
AllowedScopes = { "openid", "profile", "accounts.read", "transfers.write" }
};

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

Настройка API для интроспекции
Ваш API должен знать, как проверять эти непрозрачные токены. Вместо (или в дополнение к) проверке JWT настроим интроспекцию OAuth 2.0. Сначала определите ресурс API с секретом, который API будет использовать для аутентификации с конечной точкой интроспекции:
new ApiResource("banking_api")
{
Scopes = { "accounts.read", "transfers.write" },
ApiSecrets = { new Secret("api_secret".Sha256()) }
};

Затем в Program.cs вашего API зарегистрируйте обработчик интроспекции. Обратите внимание, что обработчик должен использовать ту же схему аутентификации, что и та, которую вы хотите использовать для интроспекции:
builder.Services.AddAuthentication("token")
.AddOAuth2Introspection("token", opts =>
{
opts.Authority = "https://identity.banking.example.com";
opts.ClientId = "banking_api";
opts.ClientSecret = "api_secret";
});

Если вам необходимо поддерживать как JWT, так и референтные токены (например, во время миграции), вы можете зарегистрировать обработчики обоих типов и использовать переадресацию для направления токенов к нужному обработчику:
builder.Services.AddAuthentication("token")
.AddJwtBearer("token", opts =>
{
opts.Authority = "https://identity.banking.example.com";
opts.Audience = "banking_api";
opts.TokenValidationParameters.ValidTypes = ["at+jwt"];
opts.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
})
.AddOAuth2Introspection("introspection", opts =>
{
opts.Authority = "https://identity.banking.example.com";
opts.ClientId = "banking_api";
opts.ClientSecret = "api_secret";
});


Отзыв токена
Теперь ваша система поддержки (или автоматизированный конвейер обнаружения угроз) сможет немедленно отозвать токен, используя конечную точку отзыва IdentityServer, которая реализует RFC 7009:
using Duende.IdentityModel.Client;

var client = new HttpClient();

var result = await client.RevokeTokenAsync(
new TokenRevocationRequest
{
Address = "https://identity.banking.example.com/connect/revocation",
ClientId = "banking_app",
ClientSecret = "secret",
Token = stolenAccessToken
});

if (result.IsError)
logger.LogError("Token revocation failed: {Error}", result.Error);

После отзыва токен удаляется из хранилища предоставленных прав IdentityServer. Следующий запрос на проверку подлинности от любого API подтвердит, что токен больше не активен.

Не забывайте: вы также можете (и должны) отозвать токен обновления пользователя, чтобы предотвратить незаметное получение клиентом нового токена доступа:
await client.RevokeTokenAsync(
new TokenRevocationRequest
{
Address = "https://identity.banking.example.com/connect/revocation",
ClientId = "banking_app",
ClientSecret = "secret",
Token = refreshToken
});

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

Источник: https://duendesoftware.com/blog/20260428-the-emergency-stop-button-implementing-immediate-token-revocation-in-dotnet-10
День 2677. #Карьера
15 Уроков Программирования от Опытного Разработчика. Начало

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

1. Придерживайтесь одного фреймворка
В первые 3-5 лет работы программистом старайтесь сосредоточиться на одном фреймворке. Игнорируйте постоянный шум и новости об очередной крутой системе. Вместо этого выберите одну и освойте её. Когда вы действительно понимаете одну технологию, будет гораздо проще изучать другие.
Многие разработчики совершают ошибку, перескакивая с одного модного фреймворка на другой. После многих лет программирования они всё ещё испытывают трудности с основами, потому что так и не освоили что-то одно досконально.
Не беспокойтесь о том, чтобы изучить всё сразу. Как только вы станете опытным разработчиком в какой-либо технологии или стеке, изучение новых станет намного проще — и многие коллеги будут считать вас опытным в том, что вы недавно изучили.

2. В случае сомнений сосредоточьтесь на основах
Фреймворки и библиотеки, которые вы используете сегодня, могут исчезнуть через 10 лет. Но основы никогда не меняются. Это особенно важно, если вы самоучка. Построение карьеры разработчика на слабых основах подобно строительству замка на песке — он в конце концов рухнет. Сосредоточьтесь на изучении структур данных, алгоритмов, решении проблем, чистом коде и проектировании систем. Эти навыки помогут адаптироваться к любой технологии в будущем.

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

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

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

6. Будьте готовы к неудачам (многочисленным)
Неудачи — часть работы разработчика. Некоторые проекты будут проваливаться. Вы можете упустить повышение, получить отказ на собеседовании или увидеть, как вашу работу отменяют. Это нормально — даже для сеньоров или технических директоров. Учитесь на неудачах. Чем больше вы пробуете, тем больше узнаёте. Не тратьте энергию на беспокойство о том, что может пойти не так. Сосредоточьтесь на решении проблем и ежедневном совершенствовании.

7. Научитесь отладке
Умение правильно отлаживать код значительно упростит вашу жизнь. Хорошие навыки отладки помогают решать критические проблемы, когда приложения ломаются. К сожалению, многие разработчики игнорируют этот навык. Если вы освоите отладку, тестирование, работу с устаревшим кодом и написание документации, вы будете выделяться как надёжный и сильный разработчик.

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

Источник:
https://medium.com/@sunil17bbmp/15-programming-lessons-i-learned-from-10-years-of-coding-321a47d14b72
👍14👎2
День 2678. #Карьера
15 Уроков Программирования от Опытного Разработчика. Окончание

Начало

8. ИИ не заменит разработчиков (в ближайшее время)
ИИ может помочь в написании кода, объяснении концепций и ускорении разработки, но ему по-прежнему нужны разработчики-люди для руководства. Большая часть работы разработчика — понимание требований, решение проблем, общение с командами и принятие решений. Это сложно автоматизировать. Поэтому вместо того, чтобы беспокоиться об ИИ, используйте его как инструмент для повышения своей продуктивности.

9. Будьте добры к начинающим
Начинающие иногда могут задавать много вопросов или оспаривать ваши идеи. Но все мы были новичками. Большинство начинающих просто пытаются учиться и делать всё возможное. Часть вашей роли как старшего разработчика — направлять и поддерживать их. Они могут ещё не знать, чего они не знают. Будьте терпеливы, помогайте им расти и относитесь к ним с уважением.

10. Работайте умнее, а не усерднее
Полезная привычка — иметь чёткое «время окончания работы», чтобы сосредоточиться на здоровье, хобби и личной жизни. Планирование дня и уменьшение отвлекающих факторов также могут помочь. Такие методы, как техника Помодоро, могут улучшить концентрацию и продуктивность. Работать эффективнее означает управлять своим временем, энергией и концентрацией, а не просто работать дольше.

11. Думайте в долгую
Всегда думайте о долгосрочных последствиях своих решений: о навыках, которые вы приобретаете, о компаниях, в которые вы устраиваетесь, и о людях, с которыми вы работаете. Многие сосредотачиваются на быстрых результатах и мгновенном вознаграждении. Но построение успешной карьеры разработчика требует времени и последовательности.
Краткосрочное планирование часто приводит к техническому долгу или постоянной смене направлений без освоения чего-либо. Успешные разработчики, как правило, те, кто остаётся последовательным и продолжает двигаться вперед, даже когда становится трудно.

12. Наибольшее повышение зарплаты часто происходит при смене работы
Во многих компаниях индексация зарплаты невелика. При смене работы повышение зарплаты иногда может быть намного выше. Поэтому многие разработчики быстрее увеличивают доход, меняя компании после получения опыта. Однако нужно быть готовым к техническим собеседованиям, задачам по программированию и обсуждениям проектирования систем.
Многие разработчики избегают собеседований, но улучшение этого навыка может открыть лучшие карьерные возможности. Каждое собеседование — это также опыт обучения.

13. Не угрожайте начальнику
Вы можете расстроиться, если не получите повышения или признания. Но избегайте угроз или шантажа начальника, чтобы получить желаемое. Даже если это сработает один раз, это может подорвать доверие и навредить вашим долгосрочным отношениям. Оставайтесь профессиональными и спокойными. Постарайтесь найти позитивные решения как для себя, так и для своего руководителя. Если ситуация не улучшится, лучшим вариантом может стать поиск возможностей в другом месте.

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

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

Источник: https://medium.com/@sunil17bbmp/15-programming-lessons-i-learned-from-10-years-of-coding-321a47d14b72
👍10👎2
День 2679. #ЗаметкиНаПолях
Когда Сценарий Выполняется Наполовину: Проектирование с Учётом Частичного Сбоя в .NET. Начало

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

Код, который выглядит нормально
Вот типичный сценарий «размещения заказа»:
internal sealed class PlaceOrder(
IOrderRepository orders,
IPaymentService payments,
IEmailService email,
IUnitOfWork uow)
{
public async Task<Result> ExecuteAsync(
PlaceOrderRequest or, CancellationToken ct)
{
var order = Order.Create(or.CustomerId, or.Items);
orders.Insert(order);

await payments
.ChargeAsync(order.Id, order.Total, ct);

await email
.SendOrderConfirmationAsync(order.Id, ct);

await uow.SaveChangesAsync(ct);
return Result.Success();
}
}

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

Если SaveChangesAsync вызывает исключение после успешного выполнения ChargeAsync, вы списали деньги клиента и потеряли заказ. Если SendOrderConfirmationAsync вызывает исключение, заказ сохраняется, и платёж проходит, но email не отправляется. А если вы попытаетесь повторить попытку, вы спишете деньги дважды. Этот вариант использования «работает», пока не ломается, а когда ломается, он, как правило, каждый раз завершается с разной ошибкой.

Три категории побочных эффектов
Прежде чем написать хотя бы одну строку кода восстановления, классифицируйте каждый побочный эффект по одной из 3 категорий:

- Транзакционные — находятся внутри транзакции вашей БД. Вставки, обновления, события домена, отправляемые в процессе.
- Внешние и обратимые — вызов API, который вы можете компенсировать. Платёж → возврат. Резервирование запасов → освобождение.
- Внешние и необратимые — отправленные email, веб-хуки, SMS-сообщения. Как только они отправлены, их не вернуть.

Категория определяет стратегию. Не существует единого правила «правильной обработки ошибок», которое охватывало бы все три.

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

Источник:
https://www.milanjovanovic.tech/blog/when-your-use-case-half-succeeds-designing-for-partial-failure-in-dotnet
👍7👎1
День 2680. #ЗаметкиНаПолях
Когда Сценарий Выполняется Наполовину: Проектирование с Учётом Частичного Сбоя в .NET. Окончание

Начало

Стратегии обработки побочных эффектов
1. Выносим транзакционные задачи на конец процесса
Первый шаг — механический. Все транзакционные операции должны быть зафиксированы в последнюю очередь, после того как все внешние вызовы либо успешно завершены, либо их сбои явно обработаны.
var order = Order.Create(…);

var charge = await
payments.ChargeAsync(…);

if (charge.IsFailure) return charge;

order.MarkPaid(charge.Value.TransactionId);

// …

orders.Insert(order);

await unitOfWork.SaveChangesAsync(ct);
return Result.Success();

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

2. Выносим необратимые побочных эффекты за пределы
Здесь применяется паттерн Outbox. Вместо прямой отправки email, сгенерируйте событие домена OrderPlaced и позвольте диспетчеру исходящих сообщений обработать его после подтверждения транзакции:
// …
orders.Insert(order);
order.Raise(new OrderPlacedEvent(order.Id));
await unitOfWork.SaveChangesAsync(ct);
// …

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

3. Делаем внешние вызовы идемпотентными или компенсируемыми
Если вызов платежа успешен, а транзакция отменена, мы взяли лишние деньги. Точно нельзя молчаливо мириться с неудачей.

Подход А: Ключи идемпотентности
Большинство платежных систем позволяют прикрепить ключ идемпотентности к платежу. Повторная попытка с тем же ключом возвращает исходный результат. Естественный ключ - ID заказа:
var charge = await payments.ChargeAsync(
new ChargeRequest
{
OrderId = order.Id,
Amount = order.Total,
IdempotencyKey = order.Id.ToString()
},
ct);

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

Подход B: Возврат через событие домена
Если повторить операцию нельзя — пользователь отказался от попытки, запрос отменён или ошибка является необратимой, деньги реальны и должны быть возвращены. Сделаем ошибку событием и вернём средства:
// …
try
{
// …
await unitOfWork.SaveChangesAsync(ct);
}
catch (Exception ex)
{
await outbox.PublishAsync(
new PaymentFailedEvent(
order.Id,
charge.Value.TransactionId,
order.Total,
Reason: ex.Message),
ct);
throw;
}
// …

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

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

Источник:
https://www.milanjovanovic.tech/blog/when-your-use-case-half-succeeds-designing-for-partial-failure-in-dotnet
👍8👎1
День 2681. #МоиИнструменты #PG
Инструменты Оптимизации Запросов в PostgreSQL. Часть 11


11. Советник в AWS Redshift
Что даёт: рекомендации по оптимизации, специфичные для Redshift.
Тип: Встроенный сервис AWS
Стоимость: Бесплатно (входит в Redshift)

Зачем нужен
Redshift предъявляет уникальные требования к оптимизации: ключи распределения, ключи сортировки, сжатие. Советник анализирует шаблоны запросов и рекомендует оптимизации.

Использование
Панель управления AWS: Redshift → Advisor recommendations
Либо CLI:
aws redshift describe-cluster-recommendations \
--cluster-identifier my-cluster

Либо SQL:
SELECT * FROM svv_redshift_advisor_recommendations;


Примеры рекомендаций
1. Добавить сжатие
Таблица: orders
Колонка: customer_notes (VARCHAR)
Текущее: Нет
Рекомендованное: LZO
Влияние: экономия места 65%
Планируемая экономия: $450/мес
Сгенерированный SQL:
ALTER TABLE orders 
ALTER COLUMN customer_notes ENCODE LZO;


2. Ключ Сортировки
Таблица: orders
Текущий: Нет
Рекомендованный: (order_date, customer_id)
Причина: 87% запросов фильтруют/сортируют по order_date
Влияние: ускорение запросов на 40%
Планируемое улучшение: сокращение времени запросов на 2.3 часа в день
Сгенерированный SQL:
CREATE TABLE orders_new (LIKE orders)
SORTKEY (order_date, customer_id);
INSERT INTO orders_new SELECT * FROM orders;
DROP TABLE orders;
ALTER TABLE orders_new RENAME TO orders;


3. Ключ распределения
Таблица: order_items
Текущий: DISTSTYLE EVEN
Рекомендованный: DISTKEY (order_id)
Причина: Частые объединения с orders по order_id
Влияние: Ускорение объединений в 3 раза
Сгенерированный SQL:
CREATE TABLE order_items_new (LIKE order_items)
DISTKEY (order_id);
INSERT INTO order_items_new SELECT * FROM order_items;
-- …


4. Обслуживание таблицы
Таблица: customers
Проблема: Замечено разбухание таблицы (75% данных на 2 узлах)
Действие: Требуются VACUUM и ANALYZE
Влияние: Улучшение производительности запросов
Сгенерированный SQL:
VACUUM SORT ONLY customers;
ANALYZE customers;


Когда использовать
- Используете AWS Redshift;
- Хотите оптимизацию специально для Redshift;
- Неясно, какие оптимизации лучше;
- Необходимо обосновать затраты на инфраструктуру (показать потенциал оптимизации).

Когда отказаться
- Не используете Redshift;
- Невозможность выполнить рекомендации (требуют перестроения таблиц).

Скрытая функция
Рекомендации по очереди запросов.
Рекомендация: Управление рабочей нагрузкой (WLM)
Текущая конфигурация: по умолчанию (1 очередь, все запросы имеют одинаковый приоритет).
Обнаруженные закономерности:
- Короткие запросы (<1 с): 85%.
- Средние запросы (1–60с): 10%.
- Длинные запросы (>60 с): 5%.
Рекомендуемая модификация WLM:
Очередь 1 (короткие): 40% памяти, параллелизм 15.
Фильтр: query_execution_time < 1с.
Очередь 2 (средняя): 40% памяти, параллелизм 5.
Фильтр: query_execution_time 1–60с.
Очередь 3 (длинная): 20% памяти, параллелизм 2.
Фильтр: query_execution_time > 60с.
Влияние:
- Короткие запросы не будут ждать длинных.
- Общее увеличение пропускной способности в 3 раза
- Снижение задержки p95: 80%

С осторожностью
Некоторые рекомендации требуют пересоздания таблиц. Изменение DISTKEY или SORTKEY требует перестроения таблицы. В таблице размером 1 ТБ это может занять несколько часов и заблокировать запись.
Более безопасный подход: использовать окно обслуживания. Назначаем на время с наименьшей нагрузкой. Сначала тестируем в среде разработки.
BEGIN;
-- 1. Создаём таблицу с оптимизациями
CREATE TABLE orders_new (LIKE orders)
SORTKEY (order_date, customer_id)
DISTKEY (customer_id);

-- 2. Копируем данные (может занять часы)
INSERT INTO orders_new SELECT * FROM orders;

-- 3. Меняем таблицы (атомарно)
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_new RENAME TO orders;

-- 4. Удаляем старую (после проверки)
DROP TABLE orders_old;
COMMIT;

Отслеживаем прогресс с помощью:
SELECT * FROM svv_vacuum_progress;


Источник:
https://medium.com/@reliabledataengineering/15-sql-optimization-tools-that-make-queries-10x-faster-8629ac451d97
День 2682. #МоиИнструменты #Copilot
Как Эффективнее Использовать GitHub Copilot. Начало
Для большинства разработчиков важно использовать подходящие функции Copilot для тех задач, которые уже стоят перед вами. Наибольший прогресс обычно проявляется, когда вы начинаете использовать чат для поиска ответов и агента для выполнения задач. Visual Studio, Visual Studio Code, Copilot CLI и облачный агент — каждый инструмент отлично подходит для разных типов задач. Вопрос не в том, какой из них «самый продвинутый», а в том, какой из них подходит для работы, которую я выполняю прямо сейчас?
 
Реальные примеры использования чата
Чат полезен, когда требуется объяснение, сравнение, планирование или целевая генерация кода на основе вашего реального проекта.
 
Пример 1: Понимание сервиса перед его изменением в VS
Предположим, вы открываете существующий сервис ASP.NET Core и обнаруживаете класс с пятью зависимостями, несколькими флагами функций и бизнес-правилами, скрытыми в длинном методе. Прежде чем его изменять, используйте чат, чтобы сориентироваться.
Промпт:
Объясни, за что отвечает сервис, определи ключевые зависимости и укажи, какие части относятся к бизнес-логике, а какие — к инфраструктурным задачам. Затем предложи безопасный первичный рефакторинг, который не изменит поведение.

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

Бонус: в VS, используйте Test Agent. В VS Code или CLI, используйте dotnet-test skill для достижения наилучших результатов. Это важный сценарий использования, поскольку нужны и результаты, и объяснение.
 
Пример 3: Планирование рефакторинга до изменения кода
Допустим, в контроллере слишком много логики, и вы хотите перенести проверку и оркестровку в сервис. Чат полезен перед редактированием.
Промпт:
Просмотри это действие контроллера и предложи рефакторинг, который перенесёт оркестровку в сервис без изменения HTTP-контракта. Покажи итоговую структуру контроллера, интерфейс сервиса и модульные тесты, которые нужно обновить.

Перед этим войдите в режим планирования, и Copilot создаст подробный план. Это хорошо работает в VS, т.к. вы уже видите соответствующие файлы и структуру решения.
 
Пример 4: Использование VS Code, когда изменение затрагивает код и конфигурацию
Распространённый сценарий в .NET — изменение API, описания OpenAPI, файла развёртывания и документации за один проход. Здесь поможет VS Code.
Промпт:
Я добавляю новый необязательный фильтр регионов к этой конечной точке. Обнови обработчик ASP.NET Core, скорректируй описание OpenAPI и найди любые конфигурации, документацию или клиентский код, которые также могут потребовать изменений.

Здесь чат начинает восприниматься не как автозаполнение, а как коллега по работе. Он может помочь вам мыслить в масштабах всего репозитория, а не только файла перед вами.
 
Пример 5: Copilot CLI для анализа ошибки сборки
Если dotnet test или dotnet build завершаются с ошибкой, CLI — естественное место для работы, поскольку ошибка уже отображается в терминале.
Промпт:
Объясни эту ошибку сборки простым языком, укажи, какой проект её вызвал, и предложи команды, которые я должен выполнить, чтобы сузить круг поиска.

Это лучше, чем копировать сообщение об ошибке в поисковую систему, потому что в CLI есть контекст вашего репозитория и цикла команд.
 
Окончание следует…
 
Источник:
https://devblogs.microsoft.com/dotnet/doing-more-with-github-copilot/
👍5👎1
День 2683. #МоиИнструменты #Copilot
Как Эффективнее Использовать GitHub Copilot. Окончание

Начало

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

Например:
Переработай этот фоновый процесс, чтобы упростить тестирование политики повторных попыток. Сохрани публичное поведение без изменений, сохрани структурированное логирование и покажи тестовые случаи, которые нужно добавить.

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

Работа с агентами
Агенты наиболее полезны, когда задача многоэтапная, ограниченная и подлежащая проверке. Вы не просто запрашиваете ответ, а просите Copilot выполнить определённую работу.

Пример 1: Устранение пробелов в тестировании функции
Предположим, в ASP.NET Core API добавлена ​​новая функция, но был протестирован только «счастливый» сценарий.
Задача:
Добавь недостающие тесты для процесса CreateOrder. Охвати ошибки валидации, обнаружение дубликатов заказов и случай таймаута платежа. Сохрани существующий стиль тестирования, не переименовывай публичные API и остановись после прохождения новых тестов.


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


Пример 3: Copilot CLI для цикла исправления и проверки
CLI полезен, когда задача естественным образом включает команды, вывод и итерацию.
Задача:
Исследуй причину сбоя dotnet test в проекте Notifications.Tests, внеси минимальное исправление, устраняющее первопричину, повторно запусти тесты и кратко опиши изменения.

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

Пример 4: Облачный агент для изменения нескольких файлов
Облачный агент особенно полезен, когда у вас есть реальная задача, которая может выполняться в фоновом режиме и возвращаться в виде черновика для проверки.
Задача:
Добавь распространение correlation ID в API и конвейер фоновых рабочих процессов. Обнови промежуточное ПО, обогащение логов и интеграционные тесты, проверяющие прохождение заголовка. Не меняй несвязанный формат логов и опиши любую последующую работу, если обнаружишь пробелы за пределами этого раздела.

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

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

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

Источник: https://devblogs.microsoft.com/dotnet/doing-more-with-github-copilot/
👍3👎2
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
День 2684. #ЗаметкиНаПолях #PowerShell
Атрибуты Валидации Параметров в PowerShell. Начало

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

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

ValidateScript
Позволяет определить пользовательскую логику проверки с помощью блока скрипта. Переменная $_ представляет проверяемое значение.
function Test-EvenNumber {
param(
[ValidateScript({ $_ % 2 -eq 0 })]
[int]$Number
)
Write-Host "Valid even number: $Number"
}

Test-EvenNumber -Number 4 #
Test-EvenNumber -Number 5 #

Также можно применять к переменным:
[ValidateScript({ $_ % 2 -ne 0 })] $oddNumber = 3
$oddNumber = 5 #
$oddNumber = 4 #

Для более понятных сообщений об ошибке, в PowerShell 6.0+ используется параметр ErrorMessage. Заместитель {0} представляет реальное значение переменной, которая не прошла валидацию:
function Set-EventDate {
param(
[ValidateScript(
{ $_ -ge (Get-Date) },
ErrorMessage = "{0} не является датой в будущем."
)]
[datetime]$EventDate
)
Write-Host "Событие запланировано на $EventDate"
}


ValidateRange
Проверяет, входит ли числовое значение в указанный диапазон.
function Set-Volume {
param(
[ValidateRange(0, 100)]
[int]$Level
)
Write-Host "Громкость $Level%"
}

Set-Volume -Level 50 #
Set-Volume -Level 150 #


ValidateSet
Ограничивает значение определённым набором вариантов.
function Get-EnvironmentInfo {
param(
[ValidateSet('Development', 'Staging', 'Production')]
[string]$Environment
)
Write-Host "Параметры среды $Environment"
}

Get-EnvironmentInfo -Environment 'Production' #
Get-EnvironmentInfo -Environment 'Test' #


ValidatePattern
Проверяет значения по регулярному выражению.
function Send-Email {
param(
[ValidatePattern('^[\w\.-]+@[\w\.-]+\.\w+$')]
[string]$EmailAddress
)
Write-Host "Отправка email на $EmailAddress"
}

Send-Email -EmailAddress 'user@example.com' #
Send-Email -EmailAddress 'invalid-email' #


ValidateLength
Проверяет, попадает ли длина строки в диапазон.
function New-Password {
param(
[ValidateLength(8, 20)]
[string]$Password
)
Write-Host "Длина пароля верная "
}

New-Password -Password 'MyP@ssw0rd' #
New-Password -Password 'short' #


ValidateCount
Проверяет количество элементов в массиве или коллекции.
function Add-Users {
param(
[ValidateCount(1, 5)]
[string[]]$UserNames
)
Write-Host "Добавление $($UserNames.Count) пользователей"
}

Add-Users -UserNames 'Alice', 'Bob' #
Add-Users -UserNames @() # (min 1)
Add-Users -UserNames 1..10 | ForEach-Object {"User$_"} # (max 5)


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

Источник:
https://www.meziantou.net/powershell-parameter-validation-attributes.htm
👍7👎3
День 2685. #ЗаметкиНаПолях #PowerShell
Атрибуты Валидации Параметров в PowerShell. Окончание

Начало

ValidateNotNull
Проверяет, что параметр не null.
function Process-Data {
param(
[ValidateNotNull()]
[object]$Data
)
Write-Host "Обработка данных: $Data"
}

Process-Data -Data 'some data' #
Process-Data -Data $null #


ValidateNotNullOrEmpty
Проверяет, что строка или коллекция не null и не пустая.
function Write-Log {
param(
[ValidateNotNullOrEmpty()]
[string]$Message
)
Write-Host "Log: $Message"
}

Write-Log -Message 'Important event' #
Write-Log -Message '' #
Write-Log -Message $null #


ValidateNotNullOrWhiteSpace
Аналогично предыдущему, проверяет, что строка не null, не пустая и не пробел.

ValidateDrive
Проверяет, что путь использует один из указанных дисков.
function Get-FileInfo {
param(
[ValidateDrive('C', 'D', 'Temp')]
[string]$Path
)
Write-Host "Получаем данные для: $Path"
}

Get-FileInfo -Path 'C:\Windows\System32' #
# если диск Temp существует
Get-FileInfo -Path 'Temp:\file.txt' #
Get-FileInfo -Path 'Z:\file.txt' #


ValidateUserDrive
Проверяет, что путь использует диск, созданный пользователем в PowerShell (а не встроенные C:, D:, и т.п.).
New-PSDrive -Name 'MyDrive' -PSProvider FileSystem -Root 'C:\MyFolder'

function Read-UserFile {
param(
[ValidateDrive()]
[ValidateUserDrive()]
[string]$Path
)
Get-Content -Path $Path
}

Read-UserFile -Path 'MyDrive:\file.txt' #
Read-UserFile -Path 'C:\file.txt' #


ValidateTrustedData
С версии 6.1.1+ проверяет, что данные происходят из надёжного источника.
function Invoke-TrustedCommand {
param(
[ValidateTrustedData()]
[string]$Command
)
Invoke-Expression $Command
}

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

Комбинирование нескольких атрибутов
К одному параметру можно применить несколько атрибутов проверки:

function Set-Configuration {
param(
[ValidateNotNullOrEmpty()]
[ValidateLength(3, 50)]
[ValidatePattern('^[a-zA-Z0-9_-]+$')]
[string]$ConfigName
)
Write-Host "Установка конфигурации: $ConfigName"
}

Set-Configuration -ConfigName 'my-config_123' #
Set-Configuration -ConfigName 'ab' #
Set-Configuration -ConfigName 'invalid@name' #

Все атрибуты валидации проверяются в порядке появления. Если любая из валидаций завершается неудачей, ошибка выбрасывается немедленно.

Источник: https://www.meziantou.net/powershell-parameter-validation-attributes.htm
👍3👎3
День 2686. #ЗаметкиНаПолях
Темпоральные таблицы в EF Core. Часть 1
Представьте, что вы создаёте платформу электронной коммерции. Клиент обращается в службу поддержки, утверждая, что его заказ был изменён без его согласия — адрес доставки изменился после оформления. Вашей операционной команде необходимо ответить на следующие вопросы:
- Как выглядел этот заказ на момент его оформления?
- Кто (или какой процесс) изменил адрес доставки и когда?
- Можно ли восстановить предыдущее состояние, не нарушая ссылочную целостность?

Наивный подход — добавить столбцы CreatedAt, UpdatedAt и ModifiedBy — показывает только дату последнего изменения. Вы не сохраняете историю. Самодельная таблица аудита изменений работает, но требует дисциплины:
- каждый разработчик должен помнить о необходимости записи в неё,
- каждый вызов SaveChanges() должен перехватываться.
Темпоральные таблицы SQL Server решают эту проблему на уровне ядра БД, а EF Core 6+ предоставляет к ним доступ через чистый, первоклассный API.

Что это?
Темпоральные таблицы автоматически поддерживают полную историю изменений строк. Ядро БД отслеживает:
- Текущее состояние каждой строки (основная таблица),
- Каждое предыдущее состояние с точным временным диапазоном, в течение которого оно было актуальным (таблица истории).
Два скрытых столбца периода datetime2 — обычно ValidFrom и ValidTo — определяют временной диапазон, в течение которого версия строки была актуальной. Они заполняются и управляются исключительно SQL Server, а не кодом приложения.

При обновлении строки старая версия перемещается в таблицу истории, при этом значение ValidTo устанавливается в текущую метку времени UTC. При удалении строки происходит то же самое — запись остаётся только в таблице истории.

Как это работает
INSERT
- В основную таблицу добавляется новая строка.
- ValidFrom устанавливается в текущее время транзакции.
- ValidTo устанавливается в 9999-12-31 23:59:59.9999999 («всё ещё актуально»).

UPDATE
SQL Server атомарно:
- Копирует текущую строку в таблицу истории, устанавливая значение ValidTo равным времени транзакции.
- Обновляет строку в основной таблице, устанавливая значение ValidFrom равным времени транзакции.

DELETE
- Текущая строка копируется в таблицу истории, устанавливая значение ValidTo равным текущему времени.
- Строка в основной таблице удаляется.

Все метки времени устанавливаются в UTC самим SQL Server. Ваше приложение не может их переопределить. Это фактически функция безопасности — она делает таблицу аудита изменений защищённой от несанкционированного изменения.

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

Источник:
https://thecodeman.net/posts/temporal-tables-efcore-auditing-history
👍17👎1
День 2687. #ЗаметкиНаПолях
Темпоральные таблицы в EF Core. Часть 2

Часть 1

Настройка темпоральных таблиц в EF Core
Системные требования:
- .NET 6+
- EF Core 6+ (Microsoft.EntityFrameworkCore.SqlServer)
- SQL Server 2016+ (или Azure SQL Database)

1. Определяем сущности
// Models/Order.cs
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
// …

public Customer Customer { get; set; } = null!;
public List<OrderItem> Items { get; set; } = [];
}

// Models/OrderItem.cs
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Product { get; set; } = "";
public int Quantity { get; set; }
public decimal Price { get; set; }

public Order Order { get; set; } = null!;
}

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

2. Настраиваем DbContext
// Data/AppDbContext.cs
public class AppDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
public DbSet<Customer> Customers => Set<Customer>();

//…

protected override void
OnModelCreating(ModelBuilder mb)
{
mb.Entity<Order>(e =>
{
e.ToTable("Orders", x => x.IsTemporal(t =>
{
t.HasPeriodStart("ValidFrom");
t.HasPeriodEnd("ValidTo");
t.UseHistoryTable("OrdersHistory", "audit");
}));

e.Property(o => o.TotalAmount)
.HasColumnType("decimal(18,2)");

e.Property(o => o.Status)
.HasMaxLength(50);
});

mb.Entity<OrderItem>(e =>
{
e.ToTable("OrderItems", x => x.IsTemporal(t =>
{
t.HasPeriodStart("ValidFrom");
t.HasPeriodEnd("ValidTo");
t.UseHistoryTable("OrderItemsHistory", "audit");
}));

e.Property(i => i.UnitPrice)
.HasColumnType("decimal(18,2)");
});
}
}

Несколько нюансов:
- схема audit - таблицы истории размещены в отдельной схеме, что позволяет сохранить основную схему чистой и упрощает управление правами доступа (например, читать audit могут только администраторы);
- явное указание имён столбцов периода - ValidFrom/ValidTo — общепринятые имена, но вы можете использовать PeriodStart/PeriodEnd, если хотите;
- и Order и OrderItem, являются временными - нам нужна полная картина.

3. Создаём миграцию
dotnet ef migrations add AddTemporalTables
dotnet ef database update

SQL Server сгенерирует что-то вроде этого:
sql 
CREATE TABLE [Orders] (
[Id] INT NOT NULL IDENTITY,
[CustomerId] INT NOT NULL,

[ValidFrom] DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
[ValidTo] DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]),
CONSTRAINT [PK_Orders] PRIMARY KEY ([Id])
)
WITH (SYSTEM_VERSIONING = ON (
HISTORY_TABLE = [audit].[OrdersHistory]
));


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

Источник:
https://thecodeman.net/posts/temporal-tables-efcore-auditing-history
👍8👎1
День 2688. #ЗаметкиНаПолях
Темпоральные таблицы в EF Core. Часть 3

1. Что это?
2. Настройка
 
Запросы
Стандартные операции EF Core (вставка, изменение, удаление) автоматически запускают операции аудита. Чтение текущих значений так же не отличается от обычного.
 
Расширенные шаблоны запросов
EF Core 7+ предоставляет 5 операторов темпоральных запросов.
 
1. TemporalAll() — полная история для сущности
var orderHistory = _db.Orders
 .TemporalAll()
 .Where(o => o.Id == orderId)
 .OrderBy(o => EF.Property<DateTime>(o, "ValidFrom"))
 .Select(o => new OrderAuditEntry
 {
   ShippingAddress = o.ShippingAddress,
   Status = o.Status,
   TotalAmount = o.TotalAmount,
   ValidFrom = EF.Property<DateTime>(o, "ValidFrom"),
   ValidTo = EF.Property<DateTime>(o, "ValidTo")
 })
 .ToListAsync();
}

Это покажет нам всю историю заказа (и ответит на претензию клиента – мы будем знать, когда изменился адрес).
 
2. TemporalAsOf() – снепшот на точку во времени
Воссоздаёт состояние сущности на определённый момент:
var asOf = DateTime.UtcNow.AddDays(-2);
var order = _db.Orders
  .TemporalAsOf(asOf)
  .Include(o => o.Items)
  .FirstOrDefaultAsync(o => o.Id == orderId);

Заметьте: при использовании TemporalAsOf() с Include() EF Core применяет временной фильтр и к связанной сущности. В результате вы получаете OrderItems, существовавшие на тот момент времени, а не текущие позиции.
 
3. TemporalBetween() — изменения в пределах временного окна
Найти все заказы, которые были изменены в течение определённого временного окна:
var timeWindow = new
{
  Start = new DateTime(2026, 6, 11, 3, 0, 0, DateTimeKind.Utc),
  End   = new DateTime(2026, 6, 11, 4, 0, 0, DateTimeKind.Utc)
};
 
var changed = await _db.Orders
 .TemporalBetween(timeWindow.Start, timeWindow.End)
 .Select(o => new
 {
   o.Id,
   o.Status,
   ValidFrom = EF.Property<DateTime>(o, "ValidFrom")
 })
 .ToListAsync();

 
4. TemporalFromTo() — перекрывающиеся временные диапазоны
Cтроки, которые были активны в течение заданного диапазона, — даже если были созданы раньше:
var ordersActiveLastWeek = await _db.Orders
  .TemporalFromTo(
    DateTime.UtcNow.AddDays(-7),
    DateTime.UtcNow)
  .Where(o => o.Status == "Processing")
  .ToListAsync();

 
5. TemporalContainedIn() — строки, полностью находящиеся в заданном диапазоне
Только строки, которые были созданы и удалены в пределах указанного диапазона:
var contained = await _db.Orders
 .TemporalContainedIn(
   DateTime.UtcNow.AddHours(-1),
   DateTime.UtcNow)
 .ToListAsync();

 
Продолжение следует…
 
Источник:
https://thecodeman.net/posts/temporal-tables-efcore-auditing-history
👍5👎1
День 2689. #ЗаметкиНаПолях
Темпоральные таблицы в EF Core. Часть 4

1. Что это?
2. Настройка
3. Запросы
 
Безопасная миграция в производственной среде
Изменение существующей таблицы на темпоральную — более деликатный процесс, чем создание её с нуля. Безопаснее всего создать ручную миграцию с чистым SQL:
public partial class TemporalOnOrders : Migration
{
  protected override void Up(MigrationBuilder mb)
  {
    // Добавляем столбцы периода
    mb.Sql(@"
  ALTER TABLE [Orders] ADD
  [ValidFrom] DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN
    CONSTRAINT DF_Orders_ValidFrom DEFAULT '2000-01-01 00:00:00.0000000',
  [ValidTo] DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN
    CONSTRAINT DF_Orders_ValidTo DEFAULT '9999-12-31 23:59:59.9999999',
  PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo]);
    ");
 
     // Включаем версионирование
    mb.Sql(@"
  ALTER TABLE [Orders]
  SET (SYSTEM_VERSIONING = ON (
    HISTORY_TABLE = [audit].[OrdersHistory],
    DATA_CONSISTENCY_CHECK = ON));
    ");
  }
 
  protected override void Down(MigrationBuilder migrationBuilder)
  {
    mb.Sql(@"
  ALTER TABLE [Orders] SET (SYSTEM_VERSIONING = OFF);
  ALTER TABLE [Orders] DROP PERIOD FOR SYSTEM_TIME;
  ALTER TABLE [Orders] DROP COLUMN [ValidFrom];
  ALTER TABLE [Orders] DROP COLUMN [ValidTo];
  DROP TABLE IF EXISTS [audit].[OrdersHistory];
   ");
  }
}

Замечание: В производственной среде вы, наверное, не захотите удалять историю, а просто отключите версионирование.
 
Производительность записи
Темпоральные таблицы не бесплатны. Каждое обновление или удаление требует от SQL Server записи дополнительной строки в таблицу истории. Для таблиц с высокой нагрузкой на запись это может быть значительным. В типичных сценариях OLTP накладные расходы - 5–15%. Для сценариев с высокой нагрузкой следует оценить, оправдывает ли необходимая детализация истории эти затраты.
 
Рост таблиц истории
Таблицы истории со временем могут значительно увеличиваться в размерах. Можно архивировать старые данные:
public async Task ArchiveOldHistoryAsync()
{
  var cutoff = DateTime.UtcNow.AddYears(-2);
 
  await _db.Database.ExecuteSqlRawAsync(@"
  -- Перемещаем в архив
  INSERT INTO [audit].[OrdersHistoryArchive]
  SELECT * FROM [audit].[OrdersHistory]
   WHERE [ValidTo] < {0};
 
  -- Удаляем историю (надо отключить версионирование)
  ALTER TABLE [Orders] SET (SYSTEM_VERSIONING = OFF);
 
  DELETE FROM [audit].[OrdersHistory]
   WHERE [ValidTo] < {0};
 
  ALTER TABLE [Orders] SET (SYSTEM_VERSIONING = ON (
   HISTORY_TABLE = [audit].[OrdersHistory]));
  ", cutoff);
}

 
Производительность чтения
Функции TemporalAll() и TemporalBetween() сканируют таблицу истории. Для панелей аудита с большими таблицами истории всегда используйте агрессивную фильтрацию и правильные индексы.
 
EF Core создаёт кластерный индекс по столбцам периода в таблице истории. Для запросов к конкретным сущностям на определённый момент времени добавьте некластерный индекс:
CREATE NONCLUSTERED INDEX IX_OrdersHistory_IdValidFrom
ON [audit].[OrdersHistory] ([Id], [ValidFrom] DESC);

 
Окончание следует…
 
Источник:
https://thecodeman.net/posts/temporal-tables-efcore-auditing-history
👍2👎1
День 2690. #ЗаметкиНаПолях
Темпоральные таблицы в EF Core. Часть 5

1. Что это?
2. Настройка
3. Запросы
4. Добавление версионирования и производительность
 
Распространенные ошибки и способы их избежать

Ошибка 1: Скафолдинг темпоральных таблиц при обратном проектировании
Если вы используете шаблон dotnet ef dbcontext для существующей темпоральной БД, EF Core может некорректно определить конфигурацию таблиц. Всегда проверяйте созданный шаблон DbContext и добавляйте конфигурацию IsTemporal() вручную, если это необходимо.
 
Ошибка 2: Мягкое удаление и темпоральные таблицы
Если ваша сущность использует паттерн мягкого удаления (флаг IsDeleted), темпоральные таблицы будут отслеживать изменения этих флагов. Т.е. «удалённые» строки по-прежнему будут находиться в основной таблице, просто с IsDeleted = true — и каждый раз, когда кто-то запрашивает историю, ему нужно будет это учитывать. Подумайте, нужны ли вам оба механизма, или достаточно жёсткого удаления и темпоральных таблиц для соответствия вашим требованиям.
 
Ошибка 3: Массовые операции в обход EF Core
Это позволяет обойти отслеживание изменений EF Core, но при этом работает с темпоральными таблицами, поскольку они задаются на уровне SQL Server:
await _db.Database.ExecuteSqlRawAsync(
 "UPDATE Orders SET Status = 'Archived' WHERE CreatedAt < {0}",
 DateTime.UtcNow.AddYears(-1));

Если вы используете стороннюю библиотеку для пакетной обработки запросов, которая напрямую вызывает SqlBulkCopy, имейте в виду, что она обходит триггеры, но НЕ темпоральные таблицы — они обрабатываются на уровне движка.
 
Ошибка 4: Миграции EF Core на темпоральные таблицы требуют осторожности
При добавлении нового столбца в темпоральную таблицу EF Core также должен добавить его в таблицу истории. EF Core обрабатывает это автоматически в миграциях, но если вы когда-либо вручную изменяли таблицу истории, миграция завершится неудачей. Никогда не изменяйте схему таблицы истории напрямую.
 
Ошибка 5: Забывание о UTC
Временные метки в таблицах всегда в формате UTC. Если ваше приложение использует местное время, преобразования могут вызвать трудно отслеживаемые ошибки в запросах к историческим данным:
//  Используется местное время
var asOf = DateTime.Now.AddDays(-1);
 
// Всегда используйте UTC
var asOf = DateTime.UtcNow.AddDays(-1);
 
var snapshot = await _db.Orders
  .TemporalAsOf(asOf)
  .FirstOrDefaultAsync(o => o.Id == orderId);

 
Когда НЕ следует использовать темпоральные таблицы
- Таблицы с высокой частотой записи (например, телеметрия в реальном времени, состояние сессии)
Накладные расходы на запись и рост истории будут проблемой. Вместо этого используйте выделенную БД временных рядов или хранилище событий.
 
- Конфиденциальность персональных данных/GDPR
Темпоральные таблицы затрудняют удаление данных — таблица истории сохраняет старые версии. Если вам необходимо поддерживать запросы «права на забвение», вам потребуется реализовать процесс удаления вручную, который отключает версионирование, очищает историю и снова включает его.
 
- Таблицы с BLOB-объектами или большими столбцами NVARCHAR(MAX)
Каждое обновление копирует всю строку в историю, включая большие поля. Это может привести к чрезвычайно быстрому росту таблицы истории.

- Требования к согласованности между базами данных
Временные метки являются индивидуальными для каждой БД. Если бизнес-транзакция охватывает несколько БД, временные записи будут иметь немного разные метки времени, что делает восстановление данных на определённый момент времени между базами данных ненадёжным.
 
Источник:
https://thecodeman.net/posts/temporal-tables-efcore-auditing-history
👎1
День 2691. #ЗаметкиНаПолях
DRY — Самый Неправильно Понимаемый Принцип Программирования

Разработчики рано усваивают принцип DRY, и почти все - неправильно. Видишь два одинаковых фрагмента кода, выдели метод, удали дубликат. Такая практика может привести к очень плохому коду:
- Вспомогательный метод, в который каждый спринт добавляется новый логический параметр.
- Базовый класс, к которому никто не хочет прикасаться, т.к. от него наследуются шесть других несвязанных между собой.
- «Общий» модуль, от которого зависят две разные части системы, поэтому ни одна из них не может измениться без другой.
Каждый случай начинался как невинная попытка не повторяться.

Что на самом деле говорит DRY
Оригинальное определение из книги «Программист-прагматик»: «Каждое знание должно иметь единственное, однозначное, авторитетное представление в системе.»
Речь о знаниях, а не о коде. Один факт о предметной области (правило скидки или формат номера счёта), должен храниться ровно в одном месте. Когда факт меняется, вы меняете его в одном месте.

Ошибка: Дедупликация кода, а не знаний
Два фрагмента кода могут выглядеть идентично, но представлять совершенно разные знания. Допустим, вы проверяете два адреса: адрес доставки клиента и адрес склада. Сегодня правила идентичны:
public bool IsValid(Address addr) =>
!string.IsNullOrWhiteSpace(addr.Street) &&
!string.IsNullOrWhiteSpace(addr.City) &&

DRY призывает выделить один валидатор и вызывать его из обоих мест. Но это разные концепции, которые пока(!) имеют общие правила. Когда складу понадобится номер погрузочных ворот, вы вернётесь в общий метод и добавите флаг, чтобы другой вызывающий код продолжал работать:
public bool IsValid(Address addr, bool hasDock = false) =>
!string.IsNullOrWhiteSpace(addr.Street) &&

(!hasDock || !string.IsNullOrWhiteSpace(addr.DockCode));

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

Границы модулей
Внутри одного класса плохой вспомогательный метод раздражает. Общий код, переходящий через границы модулей это уже структурный ущерб. Представьте модульный монолит с модулями счетов и доставки. В обоих есть класс заказа. Инженер с благими намерениями замечает, что классы имеют общие поля, и объединяет их в один, на который ссылаются оба модуля:
public class Order
{
public Guid Id { get; set; }
public string CustomerName { get; set; }
public decimal Total { get; set; }
// … другие поля из обоих модулей
}

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

Правило: Дождитесь третьего раза
Не рефакторьте первый повтор. Дождитесь третьего раза и спросите себя: если это правило изменится, должны ли все копии так же измениться?
Да — это дублирование, примените DRY. Нет — это совпадение, оставьте как есть, объединение обойдётся вам дороже позже.

Пусть код повторяется до тех пор, пока правильная абстракция не станет очевидной, потому что хорошие абстракции обнаруживаются на конкретных примерах, а не угадываются заранее. Некоторые называют это AHA (Avoid Hasty Abstractions - Избегайте поспешных абстракций).

Извлекайте информацию, когда можете дать название концепции. Реальное доменное имя: Money, TaxRate или InvoiceNumber - вероятно, представляет собой общее знание. Если лучшее имя, которое вы можете найти, — Helper, Utils или ProcessData, вы абстрагируете форму, а не знания.

Источник: https://www.milanjovanovic.tech/blog/dry-is-the-most-misunderstood-rule-in-programming
👍21👎1
День 2692. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

35. SignalR для веб-функциональности в реальном времени
«Расскажите, как реализовать систему уведомлений в реальном времени с использованием SignalR в приложении .NET? Опишите ключевые компоненты и как вы обеспечите масштабируемость и производительность системы».

Хороший ответ
Во-первых, нужно добавить библиотеку SignalR в проект .NET:
dotnet add package Microsoft.AspNetCore.SignalR


После этого создадим класс Hub, который выступает в качестве центрального координатора для входящих и исходящих сообщений. Здесь мы можем определить методы, которые клиенты будут вызывать для отправки сообщений на сервер. Сервер затем может транслировать эти сообщения другим подключённым клиентам:
public class NotificationHub : Hub
{
public async Task SendNotification(string message)
{
await Clients.All.SendAsync("ReceiveNotification", message);
}
}


В Program.cs зарегистрируем маршруты SignalR и убедимся в правильной конфигурации для использования WebSockets как метода транспортировки для лучшей производительности и снижения задержек:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();

var app = builder.Build();
app.MapHub<NotificationHub>("/notifications");

app.Run();


На стороне клиента в проект необходимо добавить клиентскую js-библиотеку signalr. Затем используем JavaScript-клиент SignalR для подключения к хабу и определения функций отправки и получения сообщений:
const conn = new signalR.HubConnectionBuilder()
.withUrl("/notifications")
.build();

conn.on("ReceiveNotification", function(message) {
console.log("New notification: " + message);
});

async function start() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
setTimeout(start, 5000);
}
};

conn.onclose(start);
start();


Для масштабирования приложения SignalR можно использовать Azure SignalR Service, который переносит управление подключениями и масштабирование на управляемый сервис, способный обрабатывать миллионы одновременных подключений. В конфигурации приложения просто изменим настройки подключения для интеграции с Azure SignalR Service:
builder.Services
.AddSignalR()
.AddAzureSignalR("строка подключения");

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎4