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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 2790. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

46. AutoMapper, метод расширения или операторы неявного приведения
«Сравните использование AutoMapper, методов расширения и операторов неявного приведения для преобразования (маппинга) объектов в приложениях .NET. Обсудите преимущества и сценарии, наиболее подходящие для каждого из этих подходов. Приведите примеры реализации для каждого метода в минимальных API».

Хороший ответ
В приложениях .NET, особенно при использовании многоуровневой архитектуры, часто возникает необходимость преобразования объектов одного типа в другой. Для решения этой задачи популярны три способа: использование AutoMapper, методов расширения и операторов неявного приведения. У каждого из них есть свои преимущества и оптимальные сценарии применения.

AutoMapper — библиотека, которая автоматически преобразует объекты одного типа в другой на основе заданных конфигураций маппинга. Её лучше всего использовать в проектах, где часто требуется выполнять сложные преобразования между типами. AutoMapper значительно сокращает объём шаблонного кода. Библиотека способна автоматически обрабатывать вложенные объекты, коллекции и сложные структуры без необходимости явно описывать правила для каждого поля, как показано в следующем примере:
var builder = WebApplication.CreateBuilder(args);

// Предположим, маппинги определены в Program.cs
builder.Services.AddAutoMapper(typeof(Program));

var app = builder.Build();

app.MapGet("/customer/{id}", async
(int id, IMapper mapper, DataContext db) =>
{
Customer customer = await db.Customers.FindAsync(id);
CustomerDto customerDto = mapper.Map<CustomerDto>(customer);
return Results.Ok(customerDto);
});

app.Run();


Методы расширения особенно полезны в сценариях сопоставления данных, когда требуется применить специфическую пользовательскую логику. Такие методы обеспечивают чёткий контроль над логикой преобразования, что критически важно в тех случаях, когда процесс трансформации не является тривиальным:
public static class MappingExtensions
{
public static CustomerDto ToDto(
this Customer customer)
{
// … сложная логика преобразования …
return new CustomerDto
{
Id = customer.Id,
Name = customer.Name,
//…
};
}
}

//…

app.MapGet("/customer/{id}",
async (int id, DataContext db) =>
{
Customer customer = await db.Customers.FindAsync(id);
CustomerDto customerDto = customer.ToDto();
return Results.Ok(customerDto);
});


Операторы неявного приведения позволяют выполнять автоматическое преобразование между двумя типами. Это удобно, когда требуется максимально упростить преобразование одного типа в другой, избегая явных вызовов методов. Они обеспечивают бесшовную интеграцию и удобство использования в коде. При разумном применении это позволяет сделать код более чистым:
public class CustomerDto
{
public int Id { get; set; }
public string Name { get; set; }
//…

public static implicit operator
CustomerDto(Customer customer)
{
return new CustomerDto
{
Id = customer.Id,
Name = customer.Name,
//…
};
}
}

//…

app.MapGet("/customer/{id}",
async (int id, DataContext db) =>
{
Customer customer = await db.Customers.FindAsync(id);
//неявное приведение
CustomerDto customerDto = customer;
return Results.Ok(customerDto);
});

Недостатком является сильная связанность между классами CustomerDto и Customer.

Часто встречающийся плохой ответ
«Просто используйте AutoMapper для любых задач маппинга; это всегда самый простой и эффективный способ преобразования объектов любого типа».

Почему это неверно
- Чрезмерная зависимость от сторонних инструментов: универсальная рекомендация использовать AutoMapper не учитывает ситуации, когда этот инструмент может создавать избыточные накладные расходы — особенно при простом маппинге.

- Игнорирование вопросов производительности: несмотря на свою мощь, AutoMapper может негативно влиять на производительность в высоконагруженных системах из-за затрат ресурсов на настройку и выполнение стратегий маппинга, что не всегда оправдано.

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

Подобный ответ обычно обусловлен недостаточным пониманием различных инструментов и стратегий маппинга либо стремлением найти простое решение без учёта конкретных требований и последствий для проекта.

Источник:
https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎9👍2
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12
День 2791. #ЗаметкиНаПолях #SQL
10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Часть 1
Большинство разработчиков используют лишь 20% возможностей SQL. SELECT, JOIN, GROUP BY — и на этом останавливаются. Однако у SQL есть и «второй уровень» — функции, позволяющие превратить страницу кода приложения или три отдельных запроса в одну лаконичную и понятную инструкцию. При этом они не являются новыми или экзотическими: они уже доступны в используемой вами БД.

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

1. Обобщённые табличные выражения (CTE)
Сложный запрос, оформленный как единая инструкция, труден для чтения, а вносить в него изменения ещё сложнее. CTE позволяет разбить его на последовательные именованные этапы с помощью ключевого слова WITH. Каждый этап представляет собой временный именованный набор данных, который можно использовать в дальнейшем ходе запроса:
WITH recent_shipments AS (
SELECT id, number, carrier, status, created_at
FROM shipments
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
),
shipment_details AS (
SELECT rs.number, rs.carrier, rs.status,
COUNT(si.id) AS total_items,
SUM(si.quantity) AS total_quantity
FROM recent_shipments rs
LEFT JOIN shipment_items si ON rs.id = si.shipment_id
GROUP BY rs.number, rs.carrier, rs.status
)
SELECT number, carrier, status, total_items, total_quantity
FROM shipment_details ORDER BY total_quantity DESC;

Здесь 2 именованные части:
- recent_shipments выбирает данные об поставках за последние 30 дней;
- shipment_details использует этот результат, присоединяя к нему данные о товарных позициях и выполняя агрегацию (подсчёт количества и объёмов). В итоговом операторе SELECT обращение ко второму CTE происходит так же, как к обычной таблице.
В результате получается запрос, который читается сверху вниз, в отличие от подхода с использованием вложенных подзапросов, которые приходится разбирать изнутри наружу.
CTE также поддерживают рекурсию (с помощью конструкции WITH RECURSIVE); это позволяет работать с иерархическими данными, такими как организационные структуры или деревья категорий.
CTE можно использовать в операторах SELECT, INSERT, UPDATE или DELETE.

2. Оконные функции
Иногда требуется выполнить вычисления по связанным строкам, сохранив при этом в результирующем наборе каждую отдельную строку. Оператор GROUP BY сворачивает строки, оставляя по одной записи на группу. Оконная функция же выполняет вычисления по набору строк (так называемому «окну»), не объединяя при этом сами строки:
SELECT number, carrier, created_at,
ROW_NUMBER() OVER (PARTITION BY carrier ORDER BY created_at DESC) AS shipment_sequence,
RANK() OVER (PARTITION BY carrier ORDER BY created_at DESC) AS shipment_rank
FROM shipments;

SELECT number, status, created_at,
LAG(status) OVER (ORDER BY created_at) AS previous_status,
LEAD(carrier) OVER (ORDER BY created_at) AS next_carrier
FROM shipments;

Первый запрос ранжирует отправления каждого перевозчика по дате. ROW_NUMBER() присваивает уникальный порядковый номер в рамках группы (заданной через PARTITION BY carrier), а RANK() делает то же самое, но при совпадении значений присваивает им одинаковый ранг.
Во втором запросе используются LAG и LEAD для обращения к предыдущей и следующей строкам (в данном случае — для получения предыдущего статуса и следующего перевозчика) без выполнения самосоединения (self-join).
Оконные функции позволяют вычислять нарастающие итоги и скользящие средние, определять ранги, а также сравнивать данные в разных строках.
Они являются стандартом SQL и поддерживаются в PostgreSQL, SQL Server, Oracle и MySQL 8+.

3. LATERAL-Соединения
Обычное соединение соединяет две таблицы на основе определённого условия. LATERAL-соединение позволяет подзапросу, расположенному справа, обращаться к столбцам таблицы, расположенной слева, и выполняется для каждой строки отдельно, что идеально подходит для задач типа «выбрать N лучших записей в каждой группе»:
-- Для каждого перевозчика выбираем одну последнюю отправку
SELECT c.carrier, s.number, s.status, s.created_at
FROM (SELECT DISTINCT carrier FROM shipments) c
CROSS JOIN LATERAL (
SELECT number, status, created_at
FROM shipments WHERE carrier = c.carrier
ORDER BY created_at DESC LIMIT 1
) s;

Для каждого конкретного перевозчика коррелирующий подзапрос выбирает одну — самую свежую — поставку (с помощью ORDER BY created_at DESC LIMIT 1).
Ключевой момент — условие WHERE carrier = c.carrier: внутренний запрос «видит» значение carrier из строки внешнего запроса, чего обычный подзапрос сделать не может.
Это наиболее элегантный способ получить «самую свежую запись в группе» или «топ-3 записи в категории» без использования оконных функций.
Примечание: в SQL Server аналогичная задача решается с помощью CROSS APPLY (или OUTER APPLY для аналога LEFT JOIN), а в PostgreSQL используются CROSS JOIN LATERAL и LEFT JOIN LATERAL.

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

Источник:
https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know
👍11👎1
🔍Тестовое собеседование с Senior C# разработчиком уже завтра

22 сентября(уже завтра!) в 19:00 по мск приходи онлайн на открытое собеседование, чтобы посмотреть на настоящее интервью на Middle C# разработчика.

Как это будет:
📂 Александр Моргунов, старший C# разработчик с опытом 7+ лет в Европейских высоконагруженных сервисах, будет задавать реальные вопросы и задачи разработчику-добровольцу
📂 Александр будет комментировать каждый ответ респондента, чтобы дать понять, чего от вас ожидает собеседующий на интервью
📂 В конце можно будет задать любой вопрос Александру

Это бесплатно. Эфир проходит в рамках менторской программы от ШОРТКАТ для C# разработчиков, которые хотят повысить свой грейд, ЗП и прокачать скиллы.

Переходи в нашего бота, чтобы получить ссылку на эфир → @shortcut_csharp_bot

Реклама.
О рекламодателе.
Please open Telegram to view this post
VIEW IN TELEGRAM
👎6👍2
День 2792. #ЗаметкиНаПолях #SQL
10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Часть 2

1-3

4. GROUPING SETS, ROLLUP и CUBE
Для отчёта часто требуется получить сразу несколько уровней агрегации: итоговые значения по перевозчику и статусу, промежуточные итоги по перевозчику и общий итог. Наивный подход предполагает объединение нескольких запросов с помощью оператора UNION ALL. Конструкции GROUPING SETS, ROLLUP и CUBE позволяют получить все эти уровни в рамках одного запроса:
SELECT carrier, status,
COUNT(*) AS shipment_count,
SUM(si.quantity) AS total_quantity
FROM shipments s
LEFT JOIN shipment_items si ON s.id = si.shipment_id
GROUP BY GROUPING SETS (
(carrier, status), -- по поставщику и статусу
(carrier), -- подытог по поставщику
(status), -- подытог по статусу
() -- общий итог
);

SELECT carrier, status,
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS shipment_count
FROM shipments
GROUP BY ROLLUP (carrier, status,
DATE_TRUNC('month', created_at)
);

Первый запрос формирует группировки, которые вам нужны: по перевозчику и статусу, только по перевозчику, только по статусу, а также пустую группу () для получения общего итога.
Оператор ROLLUP во втором запросе — это сокращённая запись для иерархических промежуточных итогов: сначала по перевозчику, затем по перевозчику и статусу, далее по перевозчику, статусу и месяцу — и наконец общий итог.
Оператор CUBE создает все возможные комбинации столбцов.
Один запрос заменяет 4, а БД вычисляет уровни за один проход, вместо того чтобы многократно сканировать таблицу.
Эти средства являются частью стандарта SQL и поддерживаются в PostgreSQL, SQL Server и Oracle.

5. Предложение FILTER в агрегатных функциях
Часто возникает необходимость подсчитать количество или сумму только для тех строк, которые удовлетворяют определённому условию, и вывести эти результаты рядом друг с другом. Предложение FILTER применяет условие к конкретной агрегатной функции, благодаря чему каждая из них обрабатывает своё подмножество данных — в рамках одной строки и за один проход по данным:
SELECT carrier,
COUNT(*) AS total_shipments,
COUNT(*) FILTER (WHERE status = 'delivered') AS delivered_count,
COUNT(*) FILTER (WHERE status = 'in_transit') AS in_transit_count,
COUNT(*) FILTER (WHERE status = 'pending') AS pending_count,
SUM(si.quantity) FILTER (WHERE status = 'delivered') AS delivered_quantity,
SUM(si.quantity) FILTER (WHERE status = 'pending') AS pending_quantity
FROM shipments s
LEFT JOIN shipment_items si ON s.id = si.shipment_id
GROUP BY carrier;

Каждое выражение COUNT(*) FILTER (WHERE …) подсчитывает только соответствующие условию строки, благодаря чему вы получаете количество отправлений со статусами «доставлено», «в пути» и «в ожидании» в виде отдельных столбцов для каждого перевозчика.
Запись COUNT(*) FILTER (WHERE status = 'delivered') читается легче, чем старый приём с использованием CASE: SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END).
Назначение конструкции FILTER гораздо более очевидно.
Примечание: FILTER поддерживается в PostgreSQL. В SQL Server и MySQL такой возможности нет — там приходится использовать CASE внутри агрегатной функции, например: COUNT(CASE WHEN status = 'delivered' THEN 1 END).

6. UPSERT (INSERT … ON CONFLICT)
Вставка строки, если она новая, и её обновление, если она уже существует — распространённая задача, для решения которой обычно требуются SELECT, условие и две ветви выполнения кода. Операция UPSERT позволяет выполнить это одной атомарной командой, исключая риск возникновения состояния гонки между проверкой и записью:
INSERT INTO shipments (id, number, order_id, address_street, address_city, address_zip, carrier, receiver_email, status, created_at, updated_at)
VALUES ('550e8400-e29b-41d4-a716-446655440000', 'SH-2024-001', 'ORD-2024-001', '123 Main St', 'New York', '10001', 'FedEx', 'customer@example.com', 'pending', NOW(), NOW())
ON CONFLICT (number) DO UPDATE
SET
carrier = EXCLUDED.carrier,
status = EXCLUDED.status,
updated_at = GREATEST(shipments.updated_at, EXCLUDED.updated_at);

Команда INSERT … ON CONFLICT (number) DO UPDATE пытается выполнить вставку; если строка с таким номером уже существует, вместо этого выполняется обновление.
Псевдотаблица EXCLUDED содержит значения, которые вы пытались вставить, поэтому запись carrier = EXCLUDED.carrier означает «использовать нового перевозчика».
Выражение GREATEST(shipments.updated_at, EXCLUDED.updated_at) позволяет сохранить более позднюю из двух временных меток.
Одна команда, никаких дублирующихся строк и никаких проблем с состоянием гонки при одновременном выполнении запросов разными клиентами.
Примечание: это синтаксис PostgreSQL. В стандарте SQL (и в таких СУБД, как SQL Server или Oracle) используется оператор MERGE, а в MySQL — INSERT … ON DUPLICATE KEY UPDATE.
Замечание: поле, по которому будет отслеживаться конфликт (number) должно иметь ограничение уникальности.

7. Поддержка JSON
Иногда требуется хранить гибкие, полуструктурированные данные — например, событие, тело веб-хука или блок настроек. PostgreSQL поддерживает JSON на уровне ядра (тип JSONB) и позволяет выполнять запросы к содержимому таких полей; благодаря этому вам не нужна отдельная документоориентированная БД для редких случаев использования JSON. Также отпадает необходимость хранить JSON в виде обычных строк и обрабатывать их на стороне бэкенда, теряя при этом все преимущества индексации:
CREATE TABLE ship_events (
id SERIAL PRIMARY KEY,
payload JSONB NOT NULL
);

-- Пример данных
INSERT INTO ship_events (payload) VALUES
('{"type":"click","coordinates":[{"x":10,"y":20},{"x":15,"y":25}]}'),
('{"type":"hover","coordinates":[{"x":5,"y":30}]}'),
('{"type":"scroll","coordinates":[{"x":0,"y":100},{"x":0,"y":200},{"x":0,"y":300}]}');

-- Выбираем поля JSON
SELECT
payload ->> 'type' AS event_type,
payload -> 'coordinates' -> 0 ->> 'x' AS first_x,
payload -> 'coordinates' -> 0 ->> 'y' AS first_y
FROM ship_events;

В таблице событий данные хранятся в формате JSONB. Запрос обращается к ним следующим образом: оператор ->> извлекает значение как текст, а оператор -> — вложенный JSON-объект или элемент массива; так, выражение payload -> 'coordinates' -> 0 ->> 'x' позволяет получить координату x первого элемента.
Данные JSONB хранятся в разобранном бинарном виде и поддерживают индексацию, что позволяет выполнять фильтрацию и извлечение информации без сканирования документов целиком.
Примечание: в SQL Server для работы с JSON используются функции JSON_VALUE и OPENJSON, а стандарт SQL предусматривает функцию JSON_TABLE (доступную в Oracle, MySQL и PostgreSQL 17+), которая преобразует JSON-массив непосредственно в строки реляционной таблицы.

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

Источник:
https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know
👍11
День 2793. #ЗаметкиНаПолях #SQL
10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Часть 3

1-3
4-7

8. Вычисляемые (генерируемые) столбцы
Если значение столбца всегда формируется на основе данных из других столбцов, его вычисление в коде приложения чревато ошибками, поскольку формулу приходится учитывать в каждом месте, где он используется. Использование генерируемого столбца позволяет перенести эту формулу в определение таблицы, благодаря чему БД вычисляет и сохраняет значение автоматически.
CREATE TABLE shipments.shipping_costs (
id SERIAL PRIMARY KEY,
shipment_id UUID NOT NULL,
base_rate DECIMAL(10,2) NOT NULL,
weight_kg DECIMAL(8,2) NOT NULL,
distance_km DECIMAL(10,2) NOT NULL,
fuel_surcharge_rate DECIMAL(5,4) NOT NULL DEFAULT 0.15,

-- Вычисляемые столбцы
weight_cost DECIMAL(10,2) GENERATED ALWAYS AS (weight_kg * 2.50) STORED,
distance_cost DECIMAL(10,2) GENERATED ALWAYS AS (distance_km * 0.85) STORED,
fuel_surcharge DECIMAL(10,2) GENERATED ALWAYS AS (base_rate * fuel_surcharge_rate) STORED,
total_cost DECIMAL(10,2) GENERATED ALWAYS AS (
base_rate
+ (weight_kg * 2.50)
+ (distance_km * 0.85)
+ (base_rate * fuel_surcharge_rate)
) STORED,

created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

FOREIGN KEY (shipment_id) REFERENCES shipments(id)
);

Значение каждого столбца, определённого как GENERATED ALWAYS AS (…) STORED, вычисляется на основе других столбцов при каждой вставке или обновлении строки. Столбец total_cost суммирует базовый тариф, стоимость с учётом веса, стоимость с учётом расстояния и топливный сбор; при этом невозможно забыть пересчитать его значение, так как в этот столбец нельзя записать данные напрямую.
Ключевое слово STORED означает, что значение сохраняется физически (и может быть проиндексировано), а не вычисляется заново при каждом чтении.
Примечание: в SQL Server такие столбцы называются вычисляемыми (computed) и описываются как total_cost AS (…), а для сохранения значения используется ключевое слово PERSISTED. В MySQL для этого применяется тот же синтаксис GENERATED ALWAYS AS, что и в PostgreSQL.

9. TABLESAMPLE
Выполнение тестового запроса к огромной таблице занимает много времени, если вам нужно лишь получить общее представление о данных, а не просматривать каждую строку. Оператор TABLESAMPLE возвращает случайную выборку из таблицы, считывая лишь её часть вместо полного сканирования:
-- Простой пример выборки
SELECT carrier, COUNT(*)
FROM shipments TABLESAMPLE SYSTEM (5)
GROUP BY carrier;

-- Случайная выборка с заданным посевом для повторяемости результатов
SELECT * FROM shipments
TABLESAMPLE BERNOULLI (10)
REPEATABLE (12345);

-- Пример с WHERE
SELECT * FROM shipments
TABLESAMPLE BERNOULLI (20)
WHERE status = 'pending';

-- Пример с соединением
SELECT s.number, s.carrier, sc.total_cost
FROM shipments s
TABLESAMPLE SYSTEM (10)
JOIN shipping_costs sc ON s.id = sc.shipment_id;

Метод TABLESAMPLE SYSTEM (5) выбирает примерно 5% данных таблицы путём считывания случайных страниц: это работает быстро, но выборка осуществляется на уровне блоков. Метод BERNOULLI (10) отбирает около 10% строк по отдельности; такой подход обеспечивает более равномерную с точки зрения статистики выборку, но выполняется медленнее.
Параметр REPEATABLE (12345) фиксирует начальное значение генератора случайных чисел, благодаря чему при каждом запуске получается одна и та же выборка, что полезно для воспроизводимых тестов.
Этот механизм предназначен для быстрой проверки, профилирования и тестирования запросов к большим таблицам без затрат ресурсов на полное сканирование.
Примечание: TABLESAMPLE входит в стандарт SQL; PostgreSQL поддерживает методы SYSTEM и BERNOULLI, а SQL Server также поддерживает TABLESAMPLE SYSTEM.

10. Частичные индексы
Индекс, охватывающий всю таблицу, требует места для хранения и замедляет операции записи — даже если ваши запросы затрагивают лишь небольшую часть строк. Частичный индекс включает в себя только те строки, которые удовлетворяют определённому условию; благодаря этому он занимает меньше места, быстрее сканируется и требует меньше ресурсов для обслуживания:
-- Частичный индекс для отправок «в пути»/«в ожидании»
CREATE INDEX idx_shipments_pending_carrier
ON shipments (carrier, created_at)
WHERE status IN ('pending', 'in_transit');

-- Частичный индекс для поставщика
CREATE INDEX idx_shipments_fedex_status
ON shipments (status, updated_at)
WHERE carrier = 'FedEx';

-- Использует idx_shipments_pending_carrier
SELECT number, carrier, created_at
FROM shipments
WHERE status = 'pending'
AND carrier = 'FedEx'
ORDER BY created_at DESC;

-- Использует idx_shipments_fedex_status
SELECT number, status, updated_at
FROM shipments
WHERE carrier = 'FedEx'
AND status IN ('delivered', 'pending')
ORDER BY updated_at DESC;

Запросы, соответствующие условиям, используют нужный индекс; поскольку каждый индекс содержит лишь часть данных таблицы, операции поиска и обслуживания выполняются быстрее.
Частичные индексы особенно эффективны для работы с «горячими» подмножествами данных — например, с активными записями, данными с фильтром is_deleted = false (мягкое удаление) или записями с определённым статусом, к которым часто обращаются. В таких случаях большинство запросов затрагивает лишь небольшую, предсказуемую часть таблицы.

Примечание: в SQL Server такие индексы называются «фильтруемыми» (filtered indexes); для их создания используется тот же синтаксис CREATE INDEX … WHERE.

Источник:
https://antondevtips.com/blog/10-rare-sql-features-every-developer-should-know
👍6👎1
День 2794. #Оффтоп #Здоровье
Сегодня будет необычный пост.

Завтра в Москве стартует конференция DotNext 2026. И от ребят из RadioDotNet поступило предложение перед открытием конференции совершить пробежку. 5 км. в прогулочном темпе. Приглашаются все желающие, даже те, кто не участвует в конференции:
- необычная движуха;
- популяризация бега среди сидячих программистов;
- посмотрим город (многие будут из других городов);
- пообщаемся.

Конференция стартует в 10 утра, так что собираемся в 7:30 у гостиницы «Холидей Инн Москва Сокольники», улица Русаковская, 24.

Побежим по парку «Сокольники», примерный маршрут ниже.

До встречи!
👍11👎3
День 2795. #ЗаметкиНаПолях #AI
Рабочий процесс с Copilot для .NET. Начало
Проблема с позиционированием Copilot как «ИИ-напарника», которое использует GitHub в маркетинговых целях, в том, что это определение верно, но неполно. Программист, впервые видящий вашу кодовую базу, — это всё ещё человек. Он может спросить: «Погоди, а вы здесь используете Service Bus именно так?» — перед тем, как уверенно сгенерировать три файла с неверными привязками. Copilot же будет уверенно генерировать код, создавая как безупречный идиоматичный код для .NET, так и галлюцинированный метод, которого не существует ни в одной версии SDK. Лучше воспринимать Copilot как джуна, который идеально помнит публичный код с GitHub, но совершенно не знает вашего конкретного стека, стандартов кодирования, или что предложенный им метод был объявлен устаревшим ещё в 2022 году. Обеспечьте его контекстом. Задайте ограничения. Проверяйте всё, что взаимодействует с внешними системами. Именно такой подход избавит вас от лишних затрат времени на отладку.

Режимы работы Copilot
1. Автодополнение. Вы печатаете, а он предлагает подсказку, которую можно принять нажатием клавиши Tab. Это отличное решение для шаблонного кода: конфигурации сущностей EF Core, запросов LINQ, добавления атрибутов к контроллерам ASP.NET Core и т.п. При работе с повторяющимися паттернами это действительно ускоряет процесс.
Недостаток: задачи, требующие специфических знаний SDK; асинхронный код, где важна правильная передача CancellationToken; и всё, что касается безопасности. В таких случаях действовать нужно более осознанно.

2. Встроенный чат (Ctrl+I в VS Code, Alt+/ в Visual Studio) — возможность отправить точечный промпт непосредственно в редакторе. Это целевой инструмент с ограниченной областью действия. Чаще всего используется для рефакторинга конкретного метода, преобразования синхронного кода в асинхронный, генерации XML-документации или чтобы выяснить, что именно делает код и зачем он это делает.

3. Окно чата (Ctrl+Alt+I в VS Code, Ctrl+\,C в VS) — ваш партнёр по размышлениям. Оно подходит как для написания кода, так и для планирования. Прежде чем приступать к реализации, откройте чат и посоветуйтесь об архитектуре: какие возможны сбои, где должна находиться логика повторных попыток, как это взаимодействует с уже используемым нами паттерном Outbox? Результатом становится не код, а ясность понимания. Это позволяет писать код, имея гораздо более чёткий план действий.

(Также существует функция Быстрого Чата — Ctrl+Shift+Alt+L в VS Code, — предназначенная для разовых вопросов без открытия полноценной панели. О ней стоит знать, даже если вы пользуетесь ей лишь изредка.)

Многие разработчики вообще не пользуются окном чата, считая это неэффективным. Но 10 минут, потраченных там, сэкономят 2 часа на последующем рефакторинге.

Режимы Ask и Agent: какой из них может вам навредить?
Режим Ask — это Copilot в роли консультанта. Он даёт советы, объясняет и предлагает решения, но не меняет файлы напрямую, пока вы сами не скопируете предложенное. Затраты времени на проверку здесь минимальны. Смело используйте этот режим для исследования, разбора незнакомого кода и планирования.

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

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

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

Для принятия архитектурных решений, анализа кода, затрагивающего множество файлов, или любых задач, связанных с логикой безопасности, используйте самую мощную модель (с лучшими способностями к рассуждению) и дайте ей необходимое время. Затраты времени на выбор правильной модели — 3 секунды. Затраты на исправление неверного результата — часы.

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

Источник:
https://levelup.gitconnected.com/a-production-ready-copilot-workflow-for-net-0ad2e7183018
👍2👎2
День 2796. #ЗаметкиНаПолях #AI
Рабочий процесс с Copilot для .NET. Продолжение

Начало

Три «S» в составлении промптов
Лучшие промпты обладают тремя характеристиками:
- Simple (простые) - одна задача за раз;
- Specific (конкретные) - явно указаны фреймворк, версия, паттерн и ограничения;
- Short (краткие) - достаточно лаконичные, чтобы полезная информация не терялась в «шуме».

Антипаттерн — «мега-промпты», пытающиеся решить всё за один раз. Например: «Создай сервис управления заказами с интеграцией Service Bus, EF Core, механизмом повторных попыток Polly, структурированным логированием и полным набором тестов».
Такой промпт выдаст результат, который технически охватывает все эти области, но в большинстве из них будет содержать ошибки.

Разбейте задачу на части: «Создай интерфейс IOrderService с методами для создания, получения и отмены заказов. Используй асинхронность повсеместно. Возвращай Result<T> — не выбрасывай исключения при передаче данных между компонентами». Это даст результат, который действительно можно использовать.

Ошибка, которая сводит всё на нет, — расплывчатый контекст. Если Copilot не знает, что вы используете .NET 10, он может предложить паттерны для .NET 6. Если он не в курсе, что у вас Polly v8, то предложит синтаксис версии 7. Конкретика в промпте ничего не стоит, а вот исправление неверных версий SDK — да.

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

- #file: позволяет указать Copilot конкретные файлы, вместо того чтобы надеяться, что он сам догадается о контексте на основе открытого в редакторе файла. Добавление в промпт ссылки на copilot-instructions.md (о нём позже) сильно меняет дело. Copilot генерирует код, соответствующий вашим текущим паттернам, а не придумывает новые.

- #terminal используется редко, хотя очень полезна. Если сборка завершается ошибкой, передайте вывод терминала прямо в запрос, вместо того чтобы пересказывать суть ошибки своими словами. Copilot видит реальный вывод компилятора и предлагает точечные исправления, а не общие рекомендации.

- @workspace открывает Copilot доступ ко всей структуре проекта, а не только к открытому файлу: «Учитывая контекст @workspace, где ещё в этой кодовой базе используется похожий паттерн, который стоит проверить на согласованность?»

- /explain — для объяснения любого легаси кода, к работе с которым собираетесь приступить;
- /tests — для создания каркаса (заглушек) тестов NUnit для готового метода;
- /fix — когда тест падает и нужна отправная точка для анализа проблемы.

copilot-instructions.md — то, что принесёт наибольшую пользу
Если вы решите внедрить что-то одно, пусть это будет данный файл. Добавьте .github/copilot-instructions.md в каждый репозиторий.

У каждого разработчика в команде свои привычки и подходы. Если оставить выбор промптов на их усмотрение, один попросит сгенерировать асинхронный код с токенами отмены, а другой об этом даже не подумает. Один знает, что вы используете FluentValidation, а другой попросит Copilot написать валидацию вручную. Один запросит обработку ошибок через Result<T>, а другой позволит Copilot генерировать исключения, пересекающие границы сервисов.

Файл copilot-instructions.md приводит подходы всех разработчиков к единому стандарту на самом раннем этапе. Он определяет ваш стек технологий, стандарты и ограничения, благодаря чему Copilot по умолчанию генерирует код, идиоматичный именно для вашей кодовой базы, а не для абстрактного проекта на .NET из обучающей выборки GitHub. Минимальная версия конфигурации для команды, работающей с Azure и .NET:
## Stack
- ASP.NET Core 8, C# 12, .NET 8
- Azure PaaS: App Service, Azure Functions, Service Bus, Blob Storage
- EF Core 8 with Azure SQL
- xUnit for testing, Moq for mocking
- Polly v8 for resilience, Serilog for structured logging
## Async Rules
- All I/O is async. No .Result, .Wait(), or Task.Run wrappers.
- CancellationToken is the last parameter on every public async method.
- Propagate tokens down the chain. Never discard them.
## Error Handling
- Never throw exceptions across service boundaries.
- Return Result<T> for operation outcomes.
- Use Polly retry + circuit breaker for all transient failures.
## Security
- No hardcoded secrets. Azure Key Vault only.
- Managed Identity for all Azure service auth. No connection string keys.
- Validate all external inputs at the API boundary. OWASP Top 10.

Будьте кратки. Copilot лучше справляется с лаконичными и конкретными инструкциями, чем с огромными массивами документации. Вот пример полноценного рабочего файла.

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

Источник:
https://levelup.gitconnected.com/a-production-ready-copilot-workflow-for-net-0ad2e7183018
👎7👍1
День 2797. #ЗаметкиНаПолях #AI
Рабочий процесс с Copilot для .NET. Окончание

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

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

2. Доверие к уверенным «галлюцинациям»
Хуже всего при работе с Copilot принять код, который выглядит правильным и компилируется, но содержит скрытую ошибку. Выдуманные пакеты NuGet легко заметить, а вот неверные сигнатуры методов, которые компилируются благодаря совпадению типов параметров, — гораздо сложнее. Любой код, затрагивающий Azure SDK, логику безопасности или внешние интеграции, перед выпуском должен вручную сверяться с официальной документацией.

3. Секреты в чате
Строкам подключения, API-ключам и данным клиентов не место в промпте. Если Copilot нужно понять структуру конфигурации, вставьте саму структуру, заменив реальные данные заглушками.

Если вы решили начать в понедельник
Не пытайтесь изменить всё и сразу. Самое эффективное действие с максимальной отдачей — это добавление файла copilot-instructions.md в ваш самый активный репозиторий. Это займет всего час, но сразу повысит продуктивность всех разработчиков, работающих с этим репозиторием, и инициирует полезное обсуждение ваших реальных стандартов разработки.

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

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

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

Репозиторий
Все материалы из этой статьи — шаблон copilot-instructions.md, каталог с более чем 30 шаблонами промптов для .NET и Azure, эталонный API на ASP.NET Core 8 с корректной настройкой Managed Identity и Service Bus, а также шаблоны Bicep (IaC) — доступны в репозитории на GitHub.

Начните с файла common-prompts.md - там собраны промпты, которые на практике доказали свою эффективность при создании рабочего кода для .NET и Azure; они систематизированы по типам задач.

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

Вопрос напоследок: расскажите о случае, когда Copilot (или любой другой инструмент) выдал «на вид правильный, но ошибочный» код, который обошёлся вам дороже всего. Галлюцинации при работе с SDK, небезопасные «быстрые решения» или некорректно развернувшаяся инфраструктура как код (IaC)?

Источник:
https://levelup.gitconnected.com/a-production-ready-copilot-workflow-for-net-0ad2e7183018
👎2👍1
День 2798. #Оффтоп
Утиная Типизация в C# с Помощью Перехватчиков. Часть 2
Некоторое время назад я выложил пост от Steven Giesel о реализации утиной типизации в C# с помощью перехватчиков (interceptors). Тогда это был лишь прототип с множеством недоработок. Теперь же это полноценная библиотека: IfItQuacks.

TL;DR
Объявите интерфейс, пометьте метод атрибутом [DuckTyped] и передавайте в него любой подходящий объект — без базового класса, без явной реализации интерфейса и без каких-либо атрибутов на самих типах:
using IfItQuacks;

public interface INamed
{
string Name { get; }
}

public class Person { public string Name => "Steven"; }
public class Mallard { public string Name => "Donald"; }

public partial class Greeter
{
[DuckTyped]
public string Greet(
INamed first,
INamed second,
string greeting = "Hello")
=> $"{greeting}, {first.Name} and {second.Name}!";
}

Console.WriteLine(new Greeter()
.Greet(new Person(), new Mallard()));
// Hello, Steven and Donald!

Ни Person, ни Mallard не знают об INamed. Генератор кода на этапе компиляции проверяет наличие у обоих типов свойства Name и перенаправляет вызов через небольшой сгенерированный адаптер. Никакой рефлексии, никакого dynamic. Если типы несовместимы, анализатор выдаст ошибку.

Можно также явно привести объекты к интерфейсу, чтобы хранить их:
List<INamed> names = [
Duck.As<INamed>(new Person()),
Duck.As<INamed>(new Mallard())
];

Подробности можно найти в документации. Далее кратко основные моменты.

Анонимные типы — это «утки»
Анонимные типы отлично «крякают» (по сути, то же самое, что предлагает TypeScript):
new Greeter().Greet(
new { Name = "Steven" },
new { Name = "Donald" });


По сути, можно писать свои тесты с «мгновенными моками»:
public interface IPerson
{
string Name { get; }
int Age { get; }
}

public partial class AgeChecker
{
[DuckTyped]
public bool IsAdult(IPerson person)
=> person.Age >= 18;
}

[Test]
public void Should_detect_adults()
{
var checker = new AgeChecker();

Assert.That(
checker.IsAdult(new { Name = "Steven", Age = 90 }), Is.True);
Assert.That(
checker.IsAdult(new { Name = "Baby Duck", Age = 1 }), Is.False);
}


Либо:
IPerson stub = 
Duck.As<IPerson>(new { Name = "Donald", Age = 90 });

List<IPerson> family =
[
Duck.As<IPerson>(new { Name = "Huey", Age = 10 }),
Duck.As<IPerson>(new { Name = "Dewey", Age = 10 }),
Duck.As<IPerson>(new { Name = "Louie", Age = 10 }),
];


Идентичность сохраняется
Адаптеры перенаправляют вызовы методов Equals, GetHashCode и ToString к обёрнутому экземпляру, благодаря чему они корректно работают в словарях и множествах. Кроме того, можно получить доступ к исходному объекту:
var person = new Person();
Duck.As<INamed>(person)
.Equals(Duck.As<INamed>(person)); // true
Duck
.Unwrap(Duck.As<INamed>(person)) is Person; // true


А также поддерживаются:
- Любой интерфейс, включая интерфейсы фреймворков: Duck.As<IDisposable>(x), параметры IEnumerable<T> и т.п.;
- Параметры интерфейса, не требующие утиной типизации (например, ILogger), продолжают принимать реализации, значения null и значения по умолчанию;
- Обобщённые интерфейсы (IContainer<T>) и обобщённые методы [DuckTyped] с выведением аргументов типа;
- События, индексаторы и реализация интерфейса по умолчанию;
- Делегаты, лямбда-выражения и группы методов для интерфейсов с одним методом;
- Статические абстрактные члены и операторы с помощью ограничений утиной типизации (where T : IAddable<T>) - включая обобщённую математику;
- Методы экземпляра и статические методы, ref/out, params, значения по умолчанию и именованные аргументы;
- Нулевая настройка: установите пакет, никаких изменений в файлах проекта не требуется.

Источник: https://steven-giesel.com/blogPost/41599817-4457-441e-b874-1a5c67f4e7cc/if-it-quacks-part-2
👍8