Опытный разработчик не так давно зашёл в .Net и поставил цель получить сертификат Microsoft. Свой ежедневный прогресс он описывает на канале .Net Разработчик. Заметки об изученном материале, советы по повышению производительности и поддержке мотивации, ин
public class RepositoryLoggerDecorator<T> : IRepository<T>
{
private readonly IRepository<T> _decoratedRepo;
public RepositoryLoggerDecorator(
IRepository<T> decoratedRepo)
{
_decoratedRepo = decoratedRepo;
}
public IEnumerable<T> GetAll()
{
Console.WriteLine("Retrieved list of users");
return _decoratedRepo.GetAll();
}
}
Декоратор должен принимать в конструкторе экземпляр декорируемого репозитория, в нашем случае это экземпляр IRepository<T>, который мы будем оборачивать. В методе GetAll() мы записываем сообщение в консоль, а затем вызываем метод GetAll() обёрнутого объекта.
Регистрация декораторов в Scrutor
Для регистрации декоратора в контейнере зависимостей в Scrutor используется метод расширения Decorate<,>():
builder.Services .Decorate<IRepository<User>, RepositoryLoggerDecorator<User>>();Первый аргумент типа метода Decorate<,>() представляет тип сервиса, который мы хотим декорировать, а второй аргумент типа — это тип декоратора. После того, как мы зарегистрируем декоратор в Scrutor, все вызовы методов IRepository<User> будут перехвачены RepositoryLoggingDecorator<User>. Замечание: В этом примере мы использовали метод Console.WriteLine() для ведения журнала, в реальном же сценарии мы бы использовали ILogger<T> и фреймворк журналирования. Источник: https://code-maze.com/dotnet-dependency-injection-with-scrutor/
builder.Services.Scan(s => s
.FromCallingAssembly()
.AddClasses(c =>
с.InNamespaces("MyApp.Services"))
.AsImplementedInterfaces()
);
Здесь мы вызываем метод расширения Scan() который выполнит сканирование сборки (в данном случае - той, из которой он вызван) и выберет сервисы для регистрации на основе предоставленного нами селектора (в данном случае из пространства имён "MyApp.Services"). Наконец, мы регистрируем выбранные нами типы в качестве подстановки для всех интерфейсов, которые они реализуют, вызывая AsImplementedInterfaces(), т.к. обычно наши зависимости представляют собой интерфейсы (типа IUserService), а не классы напрямую.
Используя широкий спектр методов расширения, которые предоставляет Scrutor, мы можем очень точно определить сервисы, которые мы хотим зарегистрировать, например, из другой сборки (библиотеки классов):
builder.Services.Scan(s => s .FromAssemblyOf<ICustomerService>() .AddClasses(c => c.AssignableTo<ICustomerService>()) .AsMatchingInterface());Здесь мы просим Scrutor сканировать сборку, в которой находится интерфейс ICustomerService, используя метод расширения FromAssemblyOf<ICustomerService>(). Затем среди всех реализаций в этой сборке мы выбираем только те, которые могут быть назначены ICustomerService. Работа с обобщёнными типами Если мы хотим добавить реализацию обобщённого типа, то вместо обобщённой реализации метода AssignableTo, мы можем использовать метод с аргументом типа:
… .AddClasses(c => c.AssignableTo(typeof(IService<>))) …Указание времени жизни зависимостей Мы можем использовать спецификаторы времени жизни зависимостей:
builder.Services.Scan(s => s … .WithTransientLifetime() );Или соответственно WithScopedLifetime() и WithSingletonLifetime(). Обработка нескольких реализаций Стратегия регистрации — это то, как Scrutor обрабатывает случаи, когда для одного и того же интерфейса существует несколько реализаций:
builder.Services.Scan(s => s … .UsingRegistrationStrategy(RegistrationStrategy.Skip) .AsImplementedInterfaces() );В этом коде, используя метод UsingRegistrationStrategy() с параметром RegistrationStrategy.Skip, мы указываем Scrutor игнорировать дополнительные реализации любого интерфейса после регистрации первой. Другие варианты: - RegistrationStrategy.Append - добавляет новую регистрацию для существующих сервисов. - RegistrationStrategy.Throw - выдаёт ошибку при попытке зарегистрировать существующий сервис. Окончание следует… Источник: https://code-maze.com/dotnet-dependency-injection-with-scrutor/
track_io_timing = on, вы получите информацию о времени выполнения для всех операций ввода-вывода.
3. Оптимальный выбор UI-инструментов: помимо pgAdmin
При изучении Postgres одним из первых вопросов, с которым вы столкнётесь, будет выбор клиента или IDE. Многие новички начинают с pgAdmin из-за его популярности и доступности. Но доступны более мощные и универсальные инструменты.
Одним из самых мощных клиентов для PostgreSQL является встроенный инструмент командной строки psql. Он содержит функции, обеспечивающие эффективное взаимодействие с базой данных, и вы найдёте его практически в любой системе, где установлен PostgreSQL.
Если вы больше склоняетесь к графическим IDE, есть несколько, которые предлагают баланс между удобством для пользователя и расширенными возможностями: DBeaver, DataGrip от JetBrains и Postico предоставляют сложные UI-интерфейсы с поддержкой выполнения запросов, визуализации данных и многого другого.
Однако независимо от того, какой графический инструмент вы выберете, потратить некоторое время на изучение всех тонкостей psql может оказаться невероятно полезным и обязательно окупится.
Источник: https://postgres.ai/blog/20230722-10-postgres-tips-for-beginners3. Ответы Возвращайте тип объектаВ большинстве случаев вызов API делается для получения или изменения данных. В последнем случае нормой является возврат изменённого ресурса. Например, если вы обновите email клиента, то в ответе вы ожидаете получить данные клиента с обновленным email. Чтобы облегчить жизнь разработчикам, чётко укажите, что именно возвращается. Например, маршрут
/v1/customers/:customer/payment_methods/:payment_methodдолжен вернуть тип PaymentMethod для данного клиента. Это должно быть очевидно из маршрута, но на всякий случай, верните тип объекта в поле "object", чтобы избежать путаницы:
{
"id": "pm_123",
"object": "payment_method",
"created": 1672217299,
"customer": "cus_123",
…
}
Это очень помогает при поиске по логам или для добавления методов защитного программирования на клиенте:
if (response.Data.Object != "payment_method")
{
// не тот объект, который ожидался
return;
}
4. Безопасность
Используйте систему разрешений
Допустим, для крупного клиента вы добавили новую функцию, чтобы они протестировали её в бета-версии. Новый маршрут не задокументирован, о нём никто не знает, поэтому можно не волноваться. Несколько недель спустя вы вносите изменения в функцию, о которых попросил крупный клиент… И получаете серию гневных писем от других пользователей, у которых всё сломалось. Оказывается, о вашем секретном маршруте узнали другие.
Теперь надо не только решать проблемы клиентов. Теперь «бета»-функция фактически выпущена, т.к. об изменениях в ней придётся сообщать всем.
Если вы хотите, чтобы закрытые API оставались закрытыми, убедитесь, что к ним нельзя получить доступ, без соответствующих разрешений. Самый простой способ — привязать систему разрешений к ключу API. Если ключ API не авторизован для использования маршрута, верните сообщение об ошибке со статусом 403.
Сделайте идентификаторы неугадываемыми
Если вы разрабатываете API, который возвращает объекты со связанными с ними идентификаторами, убедитесь, что эти их нельзя угадать или каким-либо иным образом реконструировать. Если идентификаторы просто последовательные, то в лучшем случае вы непреднамеренно выдаёте ненужную информацию о бизнесе, в худшем случае - сильно подрываете безопасность.
Например, после покупки на сайте я получил идентификатор подтверждения заказа «10». Я могу сделать два предположения:
- У вас не такой большой бизнес, как вы, вероятно, заявляете.
- Я потенциально могу получить информацию о 9 предыдущих заказах (и обо всех будущих), так как знаю их идентификаторы. Если указанный ниже маршрут не защищён системой разрешений, можно угадать идентификатор и возможно получить закрытую информацию о других ваших клиентах:
https://api.example.com/v1/orders/9Делайте идентификаторы неугадываемыми, например, используя UUID. Он, по сути, представляет собой строку случайных чисел и букв, что означает, что невозможно угадать, как будет выглядеть следующий идентификатор, основываясь на том, который у вас есть. Вы теряете в удобстве (гораздо проще говорить о «заказе 42», чем о «заказе 123e4567-e89b-12d3-a456-426614174000»), но вы компенсируете это преимуществами безопасности. Не забудьте сделать его понятным для человека, добавив префиксы объектов. Источник: https://dev.to/stripe/common-design-patterns-at-stripe-1hb4
card.pan = 4242424242424242;Но для более широкой аудитории всё же лучше подойдёт:
card.number = 4242424242424242;Это особенно важно, когда вы думаете о том, кто является аудиторией вашего API. Скорее всего, это разработчик, не знакомый с финансовыми терминами, поэтому лучше предположить, что люди не знакомы с жаргоном вашей отрасли. 2. Структура Используйте enum вместо bool Представим, что у нас есть API для модели подписки. Мы хотим, чтобы пользователи могли определить, активна подписка или отменена. Кажется разумным определить:
Subscription.canceled={true, false}
Это сработает, но что если вам нужно будет добавить приостановку подписки? Т.е. мы делаем перерыв в приеме платежей, но подписка активна и не отменена. Придётся добавить новое поле:
Subscription.canceled={true, false}
Subscription.paused={true, false}
Теперь, чтобы увидеть фактический статус подписки, нам нужно смотреть на два поля. А что, если они оба true? Можно ли приостановить подписку, которая была отменена?
Вместо этого проще сделать поле статуса с перечислением:
Subscription.status={"active", "canceled"}
Тогда приостановку легко добавить, добавив значение перечисления:
Subscription.status={"active", "canceled", "paused"}
Мы добавили функциональность, но сохранили сложность API на том же уровне, а также сделали его более описательным. Если мы когда-нибудь решим удалить функцию приостановки подписки, удалить значение перечисления всегда будет проще, чем удалить поле. Наверняка есть случаи, когда поле bool подойдёт лучше, но всегда рассматривайте возможность возникновения третьего варианта.
Используйте вложенные объекты для расширяемости
Пробуйте логически сгруппировать поля вместе. Это:
customer.address = {
line1: "Main Street 123",
city: "San Francisco",
postal_code: "12345"
};
выглядит гораздо понятнее, чем:
customer.address_line1 = "Main street 123"; customer.address_city = "San Francisco"; customer.address_postal_code: "12345";Первый вариант значительно упрощает добавление дополнительного поля позже (например, поле страны, если вы решите расширить свой бизнес для зарубежных клиентов) и гарантирует, что имена полей не станут слишком длинными. Окончание следует… Источник: https://dev.to/stripe/common-design-patterns-at-stripe-1hb4
public class ApiClient : HttpClient
{
private string _myField = "";
public string MyProperty { get; set; }
public ApiClient()
{
BaseAddress = new Uri("http://api.com");
}
public string MyMethod()
=> "hello";
}
Особенности
- Могут наследовать поведение от других классов.
- Выделение памяти для класса происходит в куче, а не в стеке. Обычно это означает больше накладных расходов, чем при использовании типов-значений. - Классы являются изменяемыми, т.е. их данные могут измениться после создания экземпляра.
Структуры
Структуры лучше всего использовать для представления простых данных с небольшим поведением или без него. Вот типичная реализация структуры:
public struct GeoLocation
{
private double _latitude;
private double _longitude;
public GeoLocation(double lat, double lng)
{
_latitude = lat;
_longitude = lng;
}
public override string ToString()
=> $"(lat: {_latitude}, lon:{_longitude})";
}
Особенности
- Хорошая практика – рассматривать структуры как неизменяемые типы.
- Размещаются в стеке, поэтому выделение/освобождение их обычно намного дешевле, чем при использовании ссылочных типов.
- Не могут наследовать от других.
См. подробнее, когда использовать структуру
Записи
Записи — это облегчённые ссылочные типы с семантикой значений. Кроме того, компилятор «из коробки» предлагает некоторые функции, такие как равенство по значению и форматирование при выводе. Вот два способа определения записи:
// обычный
public record ApiData
{
public int Id { get; init; }
}
// с позиционными параметрами
public record ApiData(int Id);
Особенности
- Записи по умолчанию - ссылочные типы.
- Можно определить конструкторы и свойства.
- Можно использовать наследование.
- Используются как небольшие неизменяемые структуры данных.
- в C# 10 появились структуры-записи (record struct), которые являются структурами, поддерживают все свойства записей, кроме наследования.
Запись стоит использовать, когда нужно инкапсулировать данные без сложного поведения (распространённый пример этого сценария — объекты передачи данных — DTO). Если структура данных будет выполнять сложное поведение и будет большим экземпляром, это признаки того, что нужно использовать класс. Кроме того, лучше выбрать класс, если понадобится изменение данных экземпляра после создания.
Источник: https://code-maze.com/csharp-should-we-use-records-classes-or-structs/pg=# create table t1 as select 1 as id; SELECT 1 pg=# select ctid, xmin, xmax, * from t1; ctid | xmin | xmax | id -------+-------+------+---- (0,1) | 47496 | 0 | 1 (1 row) pg=# update t1 set id = id where id = 1; UPDATE 1 pg=# select ctid, xmin, xmax, * from t1; ctid | xmin | xmax | id -------+-------+------+---- (0,2) | 47497 | 0 | 1 (1 row)Мы создали таблицу с одной строкой, проверили расположение живого кортежа этой строки (ctid), а затем сделали обновление, которое логически ничего не делает, т.е. не меняет значение. Но местоположение изменилось с (0,1) (страница 0, смещение 1) на (0,2). Потому что физически Postgres создал новый кортеж — новую версию строки. Понимание этого поведения Postgres поможет вам проектировать системы, работающие более эффективно. Источник: https://postgres.ai/blog/20230722-10-postgres-tips-for-beginners
public class ApplicationDbContext : DbContext
{
public override async Task<int> SaveChangesAsync(
CancellationToken ct = default)
{
var result = await
base.SaveChangesAsync(ct);
await PublishDomainEventsAsync();
return result;
}
}
Важное решение: публиковать доменные события до или после вызова SaveChangesAsync (сохранения данных в БД)?
До:
- события являются частью той же транзакции,
- немедленная согласованность данных.
После:
- события – отдельная транзакция,
- конечная согласованность, т.к. сообщения обрабатываются после исходной транзакции,
- риск несогласованности БД, т.к. обработка события может привести к сбою.
Можно решить эту проблему с помощью паттерна исходящих сообщений (Outbox), когда изменения в БД и изменения, сделанные в доменном событии сохраняются (в виде исходящих сообщений) в одной транзакции.
Метода PublishDomainEventsAsync:
private async Task PublishDomainEventsAsync()
{
var events = ChangeTracker
.Entries<Entity>()
.Select(e => e.Entity)
.SelectMany(ent =>
{
var evnts = ent.GetDomainEvents();
ent.ClearDomainEvents();
return evnts;
})
.ToList();
foreach (var ev in events)
await _mediator.Publish(ev);
}
Обработка
Нужно определить класс, реализующий INotificationHandler<T>, где T – тип доменного события. Обработчик доменного события CourseCompleted ниже публикует CourseCompletedIntegrationEvent для уведомления других систем.
public class CourseCompletedDomainEventHandler
: INotificationHandler<CourseCompleted>
{
private readonly IBus _bus;
public CourseCompletedDomainEventHandler(IBus bus)
{
_bus = bus;
}
public async Task Handle(
CourseCompleted de,
CancellationToken ct)
{
await _bus.Publish(
new CourseCompletedIntegrationEvent(de.CourseId),
ct);
}
}
Итого
- Доменные события могут помочь построить слабосвязанную систему, отделяя основную логику домена от побочных эффектов, которые можно обрабатывать асинхронно.
- Для реализации доменных событий, можно использовать библиотеки EF Core и MediatR.
- Нужно решить, когда публиковать доменные события: до или после сохранения изменений в БД.
- Публикация доменных событий после сохранения изменений в БД и паттерн Outbox для добавления транзакционных гарантий обеспечивают окончательную согласованность, но также бОльшую надёжность.
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systemsusing MediatR;
public interface IDomainEvent : INotification
{
}
public class CourseCompleted : IDomainEvent
{
public Guid CourseId { get; init; }
}
Вызов
Только сущности могут вызывать доменные события, поэтому создадим базовый класс Entity, сделав метод RaiseDomainEvent защищённым. Доменные события хранятся во внутренней коллекции, чтобы никто не мог получить к ним доступ. GetDomainEvents предназначен для получения моментального снимка коллекции, ClearDomainEvents — для очистки.
public abstract class Entity : IEntity
{
private readonly List<IDomainEvent>
_events = new();
public IReadOnlyList<IDomainEvent>
GetDomainEvents() => _events.ToList();
public void ClearDomainEvents()
=> _events.Clear();
protected void RaiseDomainEvent(
IDomainEvent domainEvent)
=> _events.Add(domainEvent);
}
Теперь сущности могут наследовать от Entity и вызывать доменные события:
public class Course : Entity
{
public Guid Id { get; private set; }
public CourseStatus Status { get; private set; }
public DateTime? Completed { get; private set; }
public void Complete()
{
Status = CourseStatus.Completed;
Completed = DateTime.UtcNow;
RaiseDomainEvent(
new CourseCompleted {
CourseId = this.Id
});
}
}
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/using-domain-events-to-build-loosely-coupled-systemspublic class BasicTests :
IClassFixture<WebApplicationFactory<Program>>
{
private WebApplicationFactory<Program> factory;
public BasicTests(
WebApplicationFactory<Program> f)
=> factory = f;
[Fact]
public async Task GetRequest()
{
var mf = factory.Services
.GetRequiredService<IMeterFactory>();
var col = new MetricCollector<double>(
mf,
"Microsoft.AspNetCore.Hosting",
"http-server-request-duration"
);
var cl = factory.CreateClient();
var resp = await cl.GetAsync("/");
Assert.Equal("Hello World!",
await resp.Content.ReadAsStringAsync());
await col.WaitForMeasurementsAsync(
minCount: 1);
Assert.Collection(col.GetMeasurementSnapshot(),
m => {
Assert.Equal("http", m.Tags["scheme"]);
Assert.Equal("GET", m.Tags["method"]);
Assert.Equal("/", m.Tags["route"]);
});
}
}
Новые счетчики в превью 6
- routing-match-success и routing-match-failure сообщают, как запрос был перенаправлен в приложении. Если запрос успешно соответствует маршруту, счётчик включает информацию о шаблоне маршрута и о том, был ли это резервный маршрут.
- diagnostics-handler-exception добавляется к ПО обработки исключений и сообщает, как оно обрабатывает необработанные исключения, включая информацию об имени исключения и результате обработки.
- http-server-unhandled-requests — сообщает, когда HTTP-запрос достигает конца конвейера промежуточного ПО и не обрабатывается приложением.
- Различные новые счётчики промежуточного ПО ограничений позволяют отслеживать количество прошедших запросов, запросов в очереди, продолжительность очереди и многое другое.
Если вам интересно попробовать метрики, разработчики .NET собрали панели мониторинга Grafana, которые сообщают о метриках ASP.NET Core, собранных Prometheus в репозитории aspnetcore-grafana
Источники:
- https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-6/
- https://devblogs.microsoft.com/dotnet/asp-net-core-updates-in-dotnet-8-preview-4/// Создание продукта POST /v1/products // Обновление продукта POST /v1/products/:id // Получение продукта GET /v1/products/:idВ примере выше маршрут для получения продукта позволяет получать только один продукт за раз. Это довольно неудобно. Добавление конечной точки для получения списка продуктов предоставит пользователю гибкость и возможность сократить количество запросов к API:
// Получаем список продуктов GET /v1/productsТо же касается других ресурсов, таких как клиенты (Customers). Как разработчик, вы хотите, чтобы ваш API был максимально предсказуемым, позволяя пользователям угадывать, какая комбинация HTTP-команд и конечных точек сработает, основываясь на предыдущем опыте работы. Будьте строго последовательны: новые маршруты должны работать во многом так же, как и существующие. Это не только позволит пользователям быстрее освоить API, но и даст ощущение хорошо продуманного, интуитивно понятного интерфейса. 2. Будьте интуитивно понятны Предположим, у нас есть объект ресурса Customer, который принимает 3 поля: имя, email и адрес. Сначала мы создаём клиента:
// Запрос
POST /v1/customers
{ "name": "John Watson",
"email": "[email protected]",
"address": "221B Baker Street" }
// Ответ
{
"id": "cus_123",
"object": "customer",
"name": "John Watson",
"email": "[email protected]",
"address": "221B Baker Street"
}
Затем мы решили обновить клиента:
// Запрос
POST /v1/customers/cus_123
{ "address": "London SW1A 1AA" }
// Ответ
{
"id": "cus_123",
"object": "customer",
"name": undefined,
"email": undefined,
"address": "London SW1A 1AA"
}
В этом запросе мы просто обновляем существующий адреса клиента. Но куда делись значения для имени и email? API сделал именно то, что ему было сказано. Он обновил значение адреса, но, поскольку значения имени и email отсутствовали в запросе на обновление, он интерпретировал их отсутствие как запрос на обнуление. Формально это правильно, но является чётким показателем того, что этот API не был разработан для людей. Вместо того, чтобы проверять, был ли передан параметр, разработчики этого API обновляют объект значением атрибута, независимо от того, был он предоставлен или нет.
API преуспевают благодаря своей интуитивности. Операции, подобные описанным выше, должны «просто работать» на основе общего предположения, а не на склонности компьютера делать именно так, как ему было сказано.
Источник: https://dev.to/stripe/designing-apis-for-humans-design-patterns-5847