Дочистить код-стайл: 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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-21
@@ -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_<id> (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_<id> (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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user