Дочистить код-стайл: var встроенных типов, приватные доки, доки интерфейсам

Гейт csharp_style_var_for_built_in_types=false:warning (ломает сборку), остаток
выправлен dotnet format IDE0008 по 5 sln (только встроенные типы). Понижено 12
новых XML-доков private/internal, добавлены <summary> членам IContainerRules и
ITenantContext, 3 англоязычных комментария переведены на русский.
This commit is contained in:
Rustam Khalimov
2026-09-11 19:00:35 +03:00
parent dde86fd63c
commit 39c9bdc1b7
51 changed files with 3204 additions and 3215 deletions
@@ -1,17 +1,15 @@
using Grpc.Core;
namespace Deal.Infrastructure.Integrations.Extensions;
/// <summary>
/// Расширения классификации gRPC-исключений клиентов автономных сервисов.
/// </summary>
internal static class RpcExceptionExtensions
{
/// <summary>
/// Ошибки коммуникации, при которых сервис считается недоступным
/// </summary>
/// <param name="exception">Исключение RPC.</param>
/// <returns>True — транспорта/контракта health нет (down); false — прикладной статус (не наша зона).</returns>
public static bool IsCommunicationFailure(this RpcException exception) =>
exception.StatusCode is StatusCode.Unavailable or StatusCode.DeadlineExceeded or StatusCode.Unimplemented;
}
using Grpc.Core;
namespace Deal.Infrastructure.Integrations.Extensions;
// Расширения классификации gRPC-исключений клиентов автономных сервисов.
internal static class RpcExceptionExtensions
{
/// <summary>
/// Ошибки коммуникации, при которых сервис считается недоступным
/// </summary>
/// <param name="exception">Исключение RPC.</param>
/// <returns>True — транспорта/контракта health нет (down); false — прикладной статус (не наша зона).</returns>
public static bool IsCommunicationFailure(this RpcException exception) =>
exception.StatusCode is StatusCode.Unavailable or StatusCode.DeadlineExceeded or StatusCode.Unimplemented;
}
@@ -1,53 +1,51 @@
using System.Net;
using System.Net.Sockets;
namespace Deal.Infrastructure.Integrations.Extensions;
/// <summary>
/// Расширения <see cref="Uri"/> для SSRF-гейта интеграций
/// </summary>
internal static class UriExtensions
{
/// <summary>
/// Проверяет, указывает ли URL на приватный/loopback/link-local адрес
/// </summary>
/// <param name="uri">Абсолютный http(s)-адрес.</param>
/// <returns>True — адрес приватный/локальный (HTTP к нему запрещён).</returns>
public static bool IsPrivateEndpoint(this Uri uri)
{
string host = uri.Host;
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (!IPAddress.TryParse(host, out IPAddress? address))
{
return false; // DNS-имя — резолв вне этого слоя
}
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
if (IPAddress.IsLoopback(address))
{
return true;
}
if (address.AddressFamily == AddressFamily.InterNetwork)
{
byte[] bytes = address.GetAddressBytes();
return bytes[0] == 10
|| (bytes[0] == 172 && bytes[1] is >= 16 and <= 31)
|| (bytes[0] == 192 && bytes[1] == 168)
|| bytes[0] == 169 && bytes[1] == 254 // link-local (включая 169.254.169.254 metadata)
|| bytes[0] == 0;
}
// IPv6: уникальные локальные (fc00::/7) и link-local (fe80::/10).
byte[] v6 = address.GetAddressBytes();
return (v6[0] & 0xFE) == 0xFC || (v6[0] == 0xFE && (v6[1] & 0xC0) == 0x80);
}
}
using System.Net;
using System.Net.Sockets;
namespace Deal.Infrastructure.Integrations.Extensions;
// Расширения Uri для SSRF-гейта интеграций
internal static class UriExtensions
{
/// <summary>
/// Проверяет, указывает ли URL на приватный/loopback/link-local адрес
/// </summary>
/// <param name="uri">Абсолютный http(s)-адрес.</param>
/// <returns>True — адрес приватный/локальный (HTTP к нему запрещён).</returns>
public static bool IsPrivateEndpoint(this Uri uri)
{
string host = uri.Host;
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (!IPAddress.TryParse(host, out IPAddress? address))
{
return false; // DNS-имя — резолв вне этого слоя
}
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
if (IPAddress.IsLoopback(address))
{
return true;
}
if (address.AddressFamily == AddressFamily.InterNetwork)
{
byte[] bytes = address.GetAddressBytes();
return bytes[0] == 10
|| (bytes[0] == 172 && bytes[1] is >= 16 and <= 31)
|| (bytes[0] == 192 && bytes[1] == 168)
|| bytes[0] == 169 && bytes[1] == 254 // link-local (включая 169.254.169.254 metadata)
|| bytes[0] == 0;
}
// IPv6: уникальные локальные (fc00::/7) и link-local (fe80::/10).
byte[] v6 = address.GetAddressBytes();
return (v6[0] & 0xFE) == 0xFC || (v6[0] == 0xFE && (v6[1] & 0xC0) == 0x80);
}
}
@@ -1,21 +1,19 @@
using Deal.Infrastructure.Integrations.Storage.Options;
namespace Deal.Infrastructure.Integrations.Storage.Extensions;
/// <summary>
/// Расширения <see cref="MinioStorageOptions"/>
/// </summary>
internal static class MinioStorageOptionsExtensions
{
/// <summary>
/// True — секция Minio заполнена настолько, что возможен Minio-адаптер.
/// </summary>
/// <param name="minio">Настройки MinIO из секции Storage:Minio.</param>
/// <returns>True — заданы Endpoint, AccessKey и SecretKey.</returns>
public static bool IsConfigured(this MinioStorageOptions minio)
{
return !string.IsNullOrWhiteSpace(minio.Endpoint)
&& !string.IsNullOrWhiteSpace(minio.AccessKey)
&& !string.IsNullOrWhiteSpace(minio.SecretKey);
}
}
using Deal.Infrastructure.Integrations.Storage.Options;
namespace Deal.Infrastructure.Integrations.Storage.Extensions;
// Расширения MinioStorageOptions
internal static class MinioStorageOptionsExtensions
{
/// <summary>
/// True — секция Minio заполнена настолько, что возможен Minio-адаптер.
/// </summary>
/// <param name="minio">Настройки MinIO из секции Storage:Minio.</param>
/// <returns>True — заданы Endpoint, AccessKey и SecretKey.</returns>
public static bool IsConfigured(this MinioStorageOptions minio)
{
return !string.IsNullOrWhiteSpace(minio.Endpoint)
&& !string.IsNullOrWhiteSpace(minio.AccessKey)
&& !string.IsNullOrWhiteSpace(minio.SecretKey);
}
}
@@ -1,17 +1,15 @@
namespace Deal.Infrastructure.Integrations.Storage.Extensions;
/// <summary>
/// Расширения <see cref="string"/> для разбора конфигурационных значений.
/// </summary>
internal static class StringExtensions
{
/// <summary>
/// Разбирает строковое значение как булев флаг конфигурации
/// </summary>
/// <param name="raw">Сырое значение настройки.</param>
/// <returns>True — значение распознано как включённое.</returns>
public static bool IsTrue(this string raw)
{
return string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase) || raw == "1";
}
}
namespace Deal.Infrastructure.Integrations.Storage.Extensions;
// Расширения string для разбора конфигурационных значений.
internal static class StringExtensions
{
/// <summary>
/// Разбирает строковое значение как булев флаг конфигурации
/// </summary>
/// <param name="raw">Сырое значение настройки.</param>
/// <returns>True — значение распознано как включённое.</returns>
public static bool IsTrue(this string raw)
{
return string.Equals(raw, "true", StringComparison.OrdinalIgnoreCase) || raw == "1";
}
}
@@ -10,7 +10,7 @@ public static class TenantSchemaMigrator
/// </summary>
public static string CreateSchemaSql(string schemaName)
{
var escaped = schemaName.Replace("\"", "\"\"");
string escaped = schemaName.Replace("\"", "\"\"");
return $"CREATE SCHEMA IF NOT EXISTS \"{escaped}\"";
}
@@ -1,20 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Deal.Infrastructure.Persistence;
/// <summary>
/// Фабрика для dotnet-ef
/// </summary>
public sealed class DealDbDesignTimeFactory : IDesignTimeDbContextFactory<DealDbContext>
{
public DealDbContext CreateDbContext(string[] args)
{
var connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
var options = new DbContextOptionsBuilder<DealDbContext>()
.UseNpgsql(connectionString)
.Options;
return new DealDbContext(options);
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Deal.Infrastructure.Persistence;
/// <summary>
/// Фабрика для dotnet-ef
/// </summary>
public sealed class DealDbDesignTimeFactory : IDesignTimeDbContextFactory<DealDbContext>
{
public DealDbContext CreateDbContext(string[] args)
{
string connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
var options = new DbContextOptionsBuilder<DealDbContext>()
.UseNpgsql(connectionString)
.Options;
return new DealDbContext(options);
}
}
@@ -1,20 +1,20 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Deal.Infrastructure.Persistence;
/// <summary>
/// Фабрика для dotnet-ef
/// </summary>
public sealed class TenantDbDesignTimeFactory : IDesignTimeDbContextFactory<TenantDbContext>
{
public TenantDbContext CreateDbContext(string[] args)
{
var connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
var options = new DbContextOptionsBuilder<TenantDbContext>()
.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable("__TenantMigrationsHistory"))
.Options;
return new TenantDbContext(options);
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Deal.Infrastructure.Persistence;
/// <summary>
/// Фабрика для dotnet-ef
/// </summary>
public sealed class TenantDbDesignTimeFactory : IDesignTimeDbContextFactory<TenantDbContext>
{
public TenantDbContext CreateDbContext(string[] args)
{
string connectionString = Environment.GetEnvironmentVariable("DEAL_PG_CONNECTION")
?? "Host=localhost;Port=5433;Database=deal;Username=deal;Password=deal_dev_password";
var options = new DbContextOptionsBuilder<TenantDbContext>()
.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable("__TenantMigrationsHistory"))
.Options;
return new TenantDbContext(options);
}
}
@@ -1,75 +1,75 @@
using System.Collections.Concurrent;
using Deal.Infrastructure.Data;
using Deal.Infrastructure.Migrations;
using Deal.Infrastructure.Persistence;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.SharedKernel.Tenants.Models;
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Deal.Infrastructure.Tenancy;
/// <summary>
/// Провижининг схемы тенанта
/// </summary>
public sealed class TenantProvisioningService(ConnectionStringProvider connectionStringProvider) : ITenantProvisioner
{
private const string TenantMigrationsHistoryTable = "__TenantMigrationsHistory";
// Семафор на схему тенанта: сериализует провижининг одного тенанта при параллельных вызовах.
// Сами операции идемпотентны, но создание схемы обязано предшествовать применению миграций,
// а два одновременных Migrate одной схемы — источник гонки. Статический: живёт дольше scoped-сервиса.
private static readonly ConcurrentDictionary<string, SemaphoreSlim> SchemaProvisionLocks = new();
/// <inheritdoc />
public async Task ProvisionAsync(TenantId tenantId, CancellationToken ct)
{
var schemaName = tenantId.SchemaName;
var provisionLock = SchemaProvisionLocks.GetOrAdd(schemaName, static _ => new SemaphoreSlim(1, 1));
await provisionLock.WaitAsync(ct);
try
{
await CreateSchemaAsync(schemaName, ct);
await ApplyTenantMigrationsAsync(tenantId, schemaName, ct);
}
finally
{
provisionLock.Release();
}
}
// Создаёт схему тенанта в БД, если её ещё нет (CREATE SCHEMA IF NOT EXISTS).
// schemaName: Имя схемы (DDL-идентификатор экранирует TenantSchemaMigrator).
// ct: Токен отмены.
private async Task CreateSchemaAsync(string schemaName, CancellationToken ct)
{
// Соединение закрывается обязательно (await using). DDL (CREATE SCHEMA) выполняется мигратор-строкой,
// если задана ConnectionStrings:DealMigrator (прод, least privilege); иначе — прикладной (dev).
await using var connection = new NpgsqlConnection(connectionStringProvider.ForSchemaDdl(null));
await connection.OpenAsync(ct);
await using var command = connection.CreateCommand();
command.CommandText = TenantSchemaMigrator.CreateSchemaSql(schemaName);
await command.ExecuteNonQueryAsync(ct);
}
// Применяет tenant-миграции: контекст на строке с search_path и историей миграций в схеме тенанта.
// tenantId: Идентификатор тенанта.
// schemaName: Имя схемы тенанта (для MigrationsHistoryTable).
// ct: Токен отмены.
private async Task ApplyTenantMigrationsAsync(
TenantId tenantId,
string schemaName,
CancellationToken ct)
{
// Строка уже с Search Path=tenant_&lt;id&gt; (ForSchemaDdl) — таблицы бессхемной модели TenantDbContext
// лягут в схему тенанта. DDL применяется мигратор-ролью при её наличии (см. ConnectionStringProvider).
var connectionString = connectionStringProvider.ForSchemaDdl(tenantId);
var options = new DbContextOptionsBuilder<TenantDbContext>()
.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable(TenantMigrationsHistoryTable, schemaName))
.Options;
await using var db = new TenantDbContext(options);
await db.Database.MigrateAsync(ct);
await DefaultContainerProvisioner.EnsureAsync(db, DateTimeOffset.UtcNow, ct);
}
}
using System.Collections.Concurrent;
using Deal.Infrastructure.Data;
using Deal.Infrastructure.Migrations;
using Deal.Infrastructure.Persistence;
using Deal.Modules.Tenants.Application.Abstractions;
using Deal.SharedKernel.Tenants.Models;
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Deal.Infrastructure.Tenancy;
/// <summary>
/// Провижининг схемы тенанта
/// </summary>
public sealed class TenantProvisioningService(ConnectionStringProvider connectionStringProvider) : ITenantProvisioner
{
private const string TenantMigrationsHistoryTable = "__TenantMigrationsHistory";
// Семафор на схему тенанта: сериализует провижининг одного тенанта при параллельных вызовах.
// Сами операции идемпотентны, но создание схемы обязано предшествовать применению миграций,
// а два одновременных Migrate одной схемы — источник гонки. Статический: живёт дольше scoped-сервиса.
private static readonly ConcurrentDictionary<string, SemaphoreSlim> SchemaProvisionLocks = new();
/// <inheritdoc />
public async Task ProvisionAsync(TenantId tenantId, CancellationToken ct)
{
string schemaName = tenantId.SchemaName;
var provisionLock = SchemaProvisionLocks.GetOrAdd(schemaName, static _ => new SemaphoreSlim(1, 1));
await provisionLock.WaitAsync(ct);
try
{
await CreateSchemaAsync(schemaName, ct);
await ApplyTenantMigrationsAsync(tenantId, schemaName, ct);
}
finally
{
provisionLock.Release();
}
}
// Создаёт схему тенанта в БД, если её ещё нет (CREATE SCHEMA IF NOT EXISTS).
// schemaName: Имя схемы (DDL-идентификатор экранирует TenantSchemaMigrator).
// ct: Токен отмены.
private async Task CreateSchemaAsync(string schemaName, CancellationToken ct)
{
// Соединение закрывается обязательно (await using). DDL (CREATE SCHEMA) выполняется мигратор-строкой,
// если задана ConnectionStrings:DealMigrator (прод, least privilege); иначе — прикладной (dev).
await using var connection = new NpgsqlConnection(connectionStringProvider.ForSchemaDdl(null));
await connection.OpenAsync(ct);
await using var command = connection.CreateCommand();
command.CommandText = TenantSchemaMigrator.CreateSchemaSql(schemaName);
await command.ExecuteNonQueryAsync(ct);
}
// Применяет tenant-миграции: контекст на строке с search_path и историей миграций в схеме тенанта.
// tenantId: Идентификатор тенанта.
// schemaName: Имя схемы тенанта (для MigrationsHistoryTable).
// ct: Токен отмены.
private async Task ApplyTenantMigrationsAsync(
TenantId tenantId,
string schemaName,
CancellationToken ct)
{
// Строка уже с Search Path=tenant_&lt;id&gt; (ForSchemaDdl) — таблицы бессхемной модели TenantDbContext
// лягут в схему тенанта. DDL применяется мигратор-ролью при её наличии (см. ConnectionStringProvider).
string connectionString = connectionStringProvider.ForSchemaDdl(tenantId);
var options = new DbContextOptionsBuilder<TenantDbContext>()
.UseNpgsql(connectionString, npgsql => npgsql.MigrationsHistoryTable(TenantMigrationsHistoryTable, schemaName))
.Options;
await using var db = new TenantDbContext(options);
await db.Database.MigrateAsync(ct);
await DefaultContainerProvisioner.EnsureAsync(db, DateTimeOffset.UtcNow, ct);
}
}