Выкладка в nuget происходит на основе триггера на тег определённого формата: как повышать версию
- Все теги должны начинаться с
v
- Для тестовой версии тег должен быть с постфиксом, например
v1.0.1-rc
- Релизный тег должен состоять только из цифр версии, например
v1.0.0
- Создание тега через git(нужно запушить его в origin) создание тега и пуш в remote
- Команды для тестового:
git checkout <название ветки>
git tag -a <название тега> -m "<описание тега>"
git push --tags
- Команды для релизного:
git tag -a <название тега> <SHA коммита> -m "<описание тега>"
git push --tags
- Команды для тестового:
- Через раздел releses(альфа версии нужно помечать соответсвующей галкой).
- При пуше, в некоторых IDE, необходимо отметить чекбокс об отправке тегов
- Создаем ветку, пушим изменения, создаем pull request.
- Добавляем на ветку тег с версией изменения
- Срабатывает workflow билдит и пушит версию(берёт из названия тега) в nuget.
- По готовности мерджим ветку в master.
- Тегаем нужный коммит мастера. Нужно обязательно описать изменения внесённые этим релизом в release notes Здесь лучше воспользоваться интерфейсом гитхаба, там удобнее редактировать текст.
- Срабатывает релизный workflow билдит и пушит в нугет релизную версию.
- В разделе Releses появляется информация о нашем релиз и release notes.
Подлкючения к redis и их настройка хранятся в appsettings.json
Для настройки redis в Startup.cs
нужно добавить 'services.AddRedis()
Для его корректной работы в appsettings.json
необходимо добавить секцию CacheManagerOptions
.
Пример:
"CacheManagerOptions": {
"HitRatioManagerUpdatePeriod": "00:05:00",
"CacheOptions": {
"FirstCache": {
"TimeToLive": "00:05:00",
"ConnectionString": "your.firstredis.connectionstring",
"RedisTimeout": "00:00:02",
"CircuitBreakerSeconds": "00:01:00",
"CircuitBreakerExceptionsCount": 20,
"CacheDbNumber": 0
},
"SecondCache": {
"TimeToLive": "01:00:00",
"ConnectionString": "your.secondredis.connectionstring",
"RedisTimeout": "00:00:02",
"CircuitBreakerSeconds": "00:01:00",
"CircuitBreakerExceptionsCount": 20,
"CacheDbNumber": 0
}
}
}
поле HitRatioManagerUpdatePeriod нужно для работы механизма сбора метрик по проценту попадания записей в кэш, но этот функционал временно не работает. В случае такого конфига будут созданы два экземпляра c разными строками подключения и разным временем жизни.
Можно указать весь ConnectionString
, заовверайдить его параметры через отдельные поля (Servers
и тд), либо просто проставить только отдельные поля. Посмотреть их можно в RedisOptions.cs
В дальнейшнем, чтобы использовать redis, нужно через DI получить экземпляр RedisProvider
и вызвать его метод GetCache
(пример):
public FirmCommentRepository(RedisProvider redisProvider)
{
IRedisCache _cache = redisProvider.GetCache(CacheNames.FirmComment.ToString());
}
Схема такая же как при работе с Redis.
Для настройки в Startup.cs
нужно добавить 'services.AddSql()
Дополнительно, можно указать кастомный таймаут к определенной процедуре из базы:
"DbManagerOptions": {
"DataBaseOptions": {
"FirstDb": {
"ConnectionString": "FirstDbConnectionString",
"Timeout": "00:00:02",
"TimeoutDictionary": {
"Very_Heavy_Procedure_Name": "5"
}
}
}
}
Можно указать весь ConnectionString
, заовверайдить его параметры через отдельные поля (Server
, Database
и тд), либо просто проставить только отдельные поля. Посмотреть их можно в DataBaseOptions.cs
В этом случае процедура на процедуру Very_Heavy_Procedure_Name
будет установлен таймаут в 5 секунд, а на все остальные 2.
Пример использования:
public FirmCommentRepository(DbProvider provider)
{
IDbWrapper _db = provider.GetDb(Databases.RateInfo.ToString());
}
common_metric_sql - collected by DapperDb and PostgressDapper
common_metric_http_client - for outgoing http requests, ConsulMetricsHttpClientWrapper uses it
common_metric_rabbitmq_in - incoming messages from rmq, used by ATI.Services.RabbitMQ and ChangeTracking
common_metric_rabbitmq_out - outgoing messages to rmq, used by ATI.Services.RabbitMQ and ChangeTracking
common_metric_repository - should be collected manually
common_metric_controller - incoming http requests, added by MeasureAttribute in controllers
common_metric_Exceptions - application exceptions
common_metric_HttpStatusCodeCounter - aspnet response codes
common_metric_redis - collected by RedisCache
common_metric_mongo - should be collected manually
common_metric_{something} - this one reserved for custom metric, if you really need it, try to keep number of unique metrics as low as possible
Так как Prometheus собирает метрики через консул, добавляем тег в конфиг консула metrics-port-*портприложения*
.
services.AddMetrics(); //или services.AddCommonMetrics();
//...
app.UseEndpoints(endpoints =>
{
//...
endpoints.MapMetricsCollection(); //Добавляем эндпоинт для сбора метрик
//...
});
app.UseMetrics(); //Добавляем мидлвару
Для использования кастомных метрик в appsettings.json
нужно определить следующую модель:
"MetricsOptions": {
"LabelsAndHeaders": {
"Лейбл метрики" : "Header HTTP-запроса"
},
},
Ключ словаря - лейбл метрики, значение - Header HTTP-запроса.
Собственно сбор:
На метод котроллера вешаем MeasureAttribute
, в который передаем название сущности, с которой работает метод.
В остальных файлах создаем нужный экземпляр MetricsFactory
оборачиваем методы в using c CreateMetricsTimer
:
private readonly MetricsFactory _metricsFactory = MetricsFactory.CreateRepositoryMetricsFactory(RepositoryName));
using (_metricsFactory.CreateMetricsTimer(EntityName))
{
Entity entity = await DoSomething();
}
Для удобства походов в другие сервисы были написаны следующие классы:
BaseServiceOptions
Если вы хотите написать адаптер для похода в чужой сервис, нужно:
- Завести класс
XServiceOptions
, отнаследовать его отBaseServiceOptions
- Завести в
appsettings.json
секциюXServiceOptions
, описать/переопределить все необходимые параметры - Зарегистрируйте его в
startup.cs
-services.ConfigureByName<XServiceOptions>
- Добавьте в
startup.cs
services.AddCustomHttpClient<XAdapter, XServiceOptions>
- Напишите свой адаптер, пример использования:
public class FirmsAdapter
{
private readonly HttpClient _httpClient;
public FirmsAdapter(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<OperationResult<FirmInfoForSettings>> GetFirmInfoAsync(int userId)
{
const string urlTemplate = "_internal/accounts/{0}";
var url = string.Format(urlTemplate, userId);
return await _httpClient.SendAsync<List<FirmInfoForSettings>>(HttpMethod.Get, url, MetricEntities.FirmService,
urlTemplate: urlTemplate);
}
}
services.AddCustomHttpClient<>()
делает следующее:
- Добавляет
AdditionalHeaders
изTServiceOptions
в каждый запрос - Добавляет
HttpLoggingHandler
, который логирует Exception/не 200 ответ от стороннего сервиса - Добавляет
HttpProxyFieldsHandler
, который проксирует поляHeadersToProxy
изTServiceOptions
в каждом запросе (вытаскивает их изIHttpContextAccessor
) - Добавляет
Retry+CircuitBreaker+Timeout Policies (Handlers)
, параметры переопределения лежат вBaseServiceOptions.cs
- Добавляет
HttpMetricsHandler
, который фиксирует время выполнения каждого запроса (каждого ретрая, если включена политика ретраев)
Есть 2 generic перегрузки:
services.AddCustomHttpClient<TAdapter, TServiceOptions>()
ДобавляетTypedHttpClient
в классTAdapter
- https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#typed-clientsservices.AddCustomHttpClient<TServiceOptions>()
ДобавляетNamedHttpClient
(по параметруTServiceOptions.ServiceName
) - https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory#named-clients
{
/// <summary>
/// Timeout for one request. If you use RetryPolicy - it will be also a timeout for one request (not total time of policy)
/// </summary>
public TimeSpan TimeOut { get; set; }
/// <summary>
/// Set 0 if you dont want to use RetryPolicy
/// </summary>
public int RetryCount { get; set; } = 3;
/// <summary>
/// Delay between retries
/// Median for spreading queries over time
/// </summary>
public TimeSpan MedianFirstRetryDelay { get; set; } = TimeSpan.FromSeconds(1);
/// <summary>
/// Http methods to retry
/// If not set - retry only GET methods
/// </summary>
public List<string> HttpMethodsToRetry { get; set; }
}
/// <summary>
/// Enable/disable CB (enabled by default)
/// </summary>
public bool CircuitBreakerEnabled = true;
/// <summary>
/// Time after which CB will be closed (will make requests)
/// </summary>
public TimeSpan CircuitBreakerDuration { get; set; } = TimeSpan.FromSeconds(2);
/// <summary>
/// Time window over which failure-success ratio calculated
/// </summary>
public TimeSpan CircuitBreakerSamplingDuration { get; set; } = TimeSpan.FromSeconds(5);
/// <summary>
/// Failure-success ratio for circuit open
/// </summary>
public double CircuitBreakerFailureThreshold { get; set; } = 0.5;
/// <summary>
/// Minimal number of actions must occur for specified sampling duration that actions was significant for statistic
/// </summary>
public int CircuitBreakerMinimumThroughput { get; set; } = 10;
Если вы вызовете AddCustomHttpClient<>
, по умолчанию будут включены все Policies. Если вы хотите выключить RetryPolicy - поставьте значение 0 у параметра RetryCount
, CB Policy - CircuitBreakerEnabled
: false.
Также можно переопределить политики выполнения конкретного запроса. Для этого в HttpClient.SendAsync
нужно передать настройку retryPolicySettings
. NOTE - если передать не NULL, тогда проверки на TServiceOptions.HttpMethodsToRetry
не будет (так как считаем, что retryPolicySettings
имеет бОльший вес; но проверка на RetryCount
- останется)
Для работы с данными, которые меняются очень редко, сделан механизм кэширования в памяти сервиса. Готовим так:
наследуем класс, в котором будут хранится данные от класса LocalCache<T>
, где T
- объект, который нужно хранить.
Добавляем в Startup.cs
services.AddLocalCache()
.
Необходимо проинициализировать все зависимости (consul, authorization, changetracker и т.д., в зависимости от того, что добавляли через services.Add...()
).
Добавляем в Startup.cs
services.AddInitializers()
. Он автоматически найдет в сборке всех наследников от IInitializers и в порядке их поля Order (по возрастанию) проинициализирует их (порядок важен, к примеру, консул должен стартовать последним).
Для запуска в Program.cs
:
using (var scope = webHost.Services.CreateScope())
{
var serviceProvider = scope.ServiceProvider;
await serviceProvider.GetRequiredService<StartupInitializer>().InitializeAsync();
}
Можно добавлять свои кастомные инициализаторы, для этого наследуемся от IInitializer
и задаем ему порядок инициализации через атрибут [InitializeOrder(Order = {ваш порядок})]
Класс OperationResult
используется во всех сервисах для соблюдения внутренних контрактов работы с базой и сторонними сервисами.
Схема похожа на http статус-коды, но немного проще.
При общении с внешними сервисами для унификации и соблюдения контракта выдачи сделан класс CommonBehavior
, все данные в контроллерах обрабатываются и приводятся к ActionResult
только через него.
При необходимости можно заменить стандартный для наших сервисов сериализатор, вызвав метод этого класса SetSerializer
и передать туда свой.
Нужно отдавать себе отчет, что класс
CommonBehavior
статический, и это действие затронет весь сервис.
В проекте есть класс ActionBuilder
, который выполняет тот же функционал, что и CommonBehavior
, но в другом стиле и с более гибкими настойками.
Сейчас испрользуетcя в сервисе иконок
Был введен недавно, и временем не проверен. Надо быть с ним осторожнее.
Для его использования в appsettings.json
нужно определить следующую модель:
"SwaggerOptions": {
"Enabled": true,
"ServiceName": "ServiceName",
"Version": "v1.1", //необязательно, по умолчанию v1.0
"ProjectsXmlNames" : ["Project-1.xml", "Project-2.xml"], //необязательно, если не указать - возьмет эти названия с папки с билдом
"SecurityApiKeyHeaders" : ["header-1","header-2"] //авторизационные хэдеры приложения
}
Контроллеры наследуем от ControllerWithOpenApi
.
На методы контроллера вешаем нужные SwaggerTag теги в атрибуте [SwaggerTag(SwaggerTag.Internal|SwaggerTag.Public)]
и
виды ответов через [ProducesResponseType(typeof(T), 200)]
.
В Startup.cs
добавляем:
services.AddAtiSwagger();
app.UseAtiSwagger();
Можно передать кастомные настройки через Action
Эти атрибуты нужны, так как при любом коде ошибки мы должны вернуть тело с описанием ошибок (ссылка).
AtiUserRequiredAttribute
. Проверяет на наличиеX-Authenticated-User-Id
и возвращает 403 с телом ошибки при его отсутствии.ValidateModelStateAttribute
. ПроверяетModelState
и возвращает 400 с телом ошибки
Схема такая же, как при работе с Redis.
Для настройки в Startup.cs
нужно добавить services.AddSlack()
Добавляем такую секцию в appsettings.json
"SlackProviderOptions": {
"SlackOptions": {
"FailedChecks": {
"AlarmChannel": "#driverchecks-alerts-staging",
"BotName": "Watcher",
"Emoji": ":dicaprio:",
"SlackAddress": "https://hooks.slack.com",
"WebHookUri": "url",
"AlertsEnabled": true
},
"KonturFullCheck": {
"AlarmChannel": "#kontur_full_check_request-staging",
"BotName": "Watcher",
"Emoji": ":dicaprio:",
"SlackAddress": "https://hooks.slack.com",
"WebHookUri": "url",
"AlertsEnabled": true
}
}
}
Пример использования:
public KonturFullCheckAlertSender(SlackProvider slackProvider)
{
_slackAdapter = slackProvider.GetAdapter(SlackChannel.KonturFullCheck.ToString());
}
Для использования NLog нужно:
- Создать секцию NLogOptions в
appsettings
- Подключить NLog
.UseNLog()
в Program.cs - Настроить NLog после инициализации ConfigurationManager в Startup.cs
var nLogOptions = ConfigurationManager.ConfigurationRoot.GetSection("NLogOptions").Get<NLogOptions>();
var nLogConfigurator = new NLogConfigurator(nLogOptions);
nLogConfigurator.ConfigureNLog();
Структура секции NLogOptions в appsettiings, default значения можно опустить:
"NLogOptions": {
"ThrowExceptions": false, //default
"AddGeneralAttributes": true, //default | Использовать ли общие атрибуты, список можно посмотреть в NLogConfigurator.cs:35
"Variables": [
{
"Name": "applicationName",
"Value": "debug"
}
],
"Attributes": [
{
"Name": "custom",
"Layout": "custom",
"EscapeUnicode": false, //default
"EncodeJson": true, //default
"IncludeEmptyValue": false //default
}
],
"Rules": [
{
"TargetName": "jsonFile",
"MinLevel": "Warn", //default
"MaxLevel": "Off", //default | Off значит без верхнего ограничения
"LoggerNamePattern": "*" //default
}
],
"FileTargets": [
{
"Name": "jsonFile",
"FileName": "${basedir}/Log/NLog.Errors.json", //default
"MaxArchiveFiles": 7, //default
"ArchiveNumbering": "Date", //default
"ArchiveEvery": "Day", //default
"ArchiveDateFormat": "yyyyMMdd", //default
"ArchiveFileName": "${basedir}/Log/NLog.Error.{##}.json", //default
"AddGeneralAttributes": true, //default | аналогично AddGeneralAttributes в корне, но влияет только на этот таргет
"Attributes": [
{
// Специфичные для таргета атрибуты
}
]
}
],
"NetworkTargets": [
{
"Name": "logStash",
"address": "udp4://network-target.address",
"keepConnection": true, //default
"onOverflow": "Split", //default
"newline": true, //default
"Attributes": [
{
// Специфичные для таргета атрибуты
}
]
}
],
"LoggedRequestHeader" : ["header-1","header-2"] //логируемые хэдеры входящих HTTP запросов
}
Данный блок конфигурации используется для конфигурирования данных уровня всего приложения В приложении можно вызывать
ServiceVariables.Variables
Так же имеются предопределенные поля:
ServiceVariables.ServiceAsClientHeaderName
ServiceVariables.ServiceAsClientName
ServiceVariables.DefaultLocale
ServiceVariables.SupportedLocales
Структура секции ServiceVariables
"ServiceVariablesOptions": {
"Variables": {
//Передается в каждый исходящий HTTP Запрос ConsulMetricsHttpClientWrapper в качестве header'a со значением ServiceAsClientName
"ServiceAsClientHeaderName": "ClientNameHeader",
"ServiceAsClientName": "ServiceName", //имя сервиса при исходящих HTTP запросах
"DefaultLocale":"ru", //локаль, использующаяся по умолчанию
"VarName-3":"Var value 3", //Дополнительные параметры
"VarName-4":"Var value 4" //Дополнительные параметры
},
"SupportedLocales":["ru","en"] //список поддерживаемых сервисом локалей
}
В Startup.cs
вызываем
services.AddServiceVariables();
Поддержана работа по локализации, для работы нужно добавить DefaultLocale
и SupportedLocales
в ServiceVariablesOptions
и использовать в Startup (в самом начале)
app.UseAcceptLanguageLocalization();
Теперь, если заголовок Accept-Language при http запросе или хэдер accept_language rmq сообщения будет передан, то получить значение культуры можно в любом месте приложения путем вызова
LocaleHelper.GetLocale();
Значение хэдеров, без парсинга, хранится в FlowContext и его можно получить
FlowContext<RequestMetaData>.AccessLanguage;
Небольшой хелпер для локализации строк, переводы можно хранить прямо в коде. Подходит если строк для перевода относительно мало.
Работает только с локалями перечисленными в ServiceVariables.SupportedLocales
, текущую локаль определяет через LocaleHelper.GetLocale()
- Добавить в Startup.cs
services.AddInCodeLocalization();
- Реализовать
IInCodeLocalization
, есть 2 варианта:- для всех локалей кроме дефолтной
в качестве ключа использовать значение в дефолтной локали,
более наглядно, т.к. в коде по месту используются не "NortWestRegionId", а сам текст в дефолтной локали
public class EnLocalization : IInCodeLocalization { public string Locale { get; } = new("en"); public ReadOnlyDictionary<string, string> LocalizedStrings => new(new Dictionary<string, string> { { "Северо-Западный фед.округ", "Northwestern Federal District" }, }); } // использование // в дефолтной локали вернёт переданный ключ "Северо-Западный фед.округ" _inCodeLocalizer["Северо-Западный фед.округ"]
- для каждой поддерживаемой локали
public class RuLocalization : IInCodeLocalization { public string Locale { get; } = new("ru"); public ReadOnlyDictionary<string, string> LocalizedStrings => new(new Dictionary<string, string> { { "NortWestRegionId", "Северо-Западный фед.округ" }, }); } public class EnLocalization : IInCodeLocalization { public string Locale { get; } = new("en"); public ReadOnlyDictionary<string, string> LocalizedStrings => new(new Dictionary<string, string> { { "NortWestRegionId", "Northwestern Federal District" }, }); } // использование _inCodeLocalizer["NortWestRegionId", false]
- для всех локалей кроме дефолтной
в качестве ключа использовать значение в дефолтной локали,
более наглядно, т.к. в коде по месту используются не "NortWestRegionId", а сам текст в дефолтной локали
Небольшой сервис для проверки на xss инъекции
Добавить в Startup.cs
app.UseXssValidation();
Над котроллером написать
[XssInputValidationFilter]
- Добавить в Startup.cs
services.AddXssValidationAttribute();
- Над свойствами в классе добавить
[XssValidate]
Атрибут который экранирует Xss, либо выдает ошибку на моменте валидации модели. Атрибут добавляется к полям.
Чтобы валидации прошла, на метод, в котором требуется валидации модели с аттрибутом XssSanitizer
нужно добавить аттрибут
[ValidateModelState]
По умолчанию атрибут будет эранировать xss. Для того чтобы была ошибка на моменте валидации, нужно передать параметр в атрибут следующим образом
[XssSanitizer(IsReplace = false)]