diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 236e095..60476d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,33 +1,73 @@ -name: Deploy NuGet Package +name: Publish NuGet Package env: - PROJECT_PATH: './src/SharedKernel/SharedKernel.csproj' - OUTPUT_DIR: 'nupkgs' - NUGET_SOURCE: 'https://api.nuget.org/v3/index.json' - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + PROJECT_PATH: src/SharedKernel/SharedKernel.csproj + OUTPUT_DIR: nupkgs + NUGET_SOURCE: https://api.nuget.org/v3/index.json on: push: - branches: - - main + branches: [main] + +permissions: + contents: read + jobs: - deploy: + publish: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - - name: Setup .NET Core + - name: Setup .NET uses: actions/setup-dotnet@v5 with: global-json-file: global.json + - name: Restore + run: dotnet restore ${{ env.PROJECT_PATH }} + - name: Build - run: dotnet build ${{ env.PROJECT_PATH }} + run: dotnet build ${{ env.PROJECT_PATH }} --no-restore --configuration Release + +# - name: Test +# run: dotnet test --no-build --configuration Release --verbosity normal - name: Pack - run: dotnet pack ${{ env.PROJECT_PATH }} --output ${{ env.OUTPUT_DIR }} + run: dotnet pack ${{ env.PROJECT_PATH }} --no-build --configuration Release --output ${{ env.OUTPUT_DIR }} - name: Publish - run: dotnet nuget push ${{ env.OUTPUT_DIR }}/*.nupkg -k ${{ env.NUGET_API_KEY }} -s ${{ env.NUGET_SOURCE }} + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + shell: bash + run: | + set -euo pipefail + + if [ -z "${NUGET_API_KEY:-}" ]; then + echo "NUGET_API_KEY secret is not set." + exit 1 + fi + + shopt -s nullglob + + nupkgs=( "${{ env.OUTPUT_DIR }}"/*.nupkg ) + snupkgs=( "${{ env.OUTPUT_DIR }}"/*.snupkg ) + + if [ ${#nupkgs[@]} -eq 0 ]; then + echo "No .nupkg files found in ${{ env.OUTPUT_DIR }}" + ls -la "${{ env.OUTPUT_DIR }}" || true + exit 1 + fi + + dotnet nuget push "${nupkgs[@]}" \ + --api-key "$NUGET_API_KEY" \ + --source "${{ env.NUGET_SOURCE }}" \ + --skip-duplicate + + if [ ${#snupkgs[@]} -gt 0 ]; then + dotnet nuget push "${snupkgs[@]}" \ + --api-key "$NUGET_API_KEY" \ + --source "${{ env.NUGET_SOURCE }}" \ + --skip-duplicate + fi \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fd38ec --- /dev/null +++ b/README.md @@ -0,0 +1,688 @@ +# Pandatech.SharedKernel + +Opinionated ASP.NET Core 10 infrastructure library for PandaTech projects. It consolidates logging, OpenAPI, +validation, CORS, SignalR, telemetry, health checks, maintenance mode, and resilience into a single package so every +service starts from the same baseline. + +The package is publicly available but is designed for internal use. If you want to adopt it, fork the repository and +customize it to your own conventions. + +Requires **.NET 10.0**. Uses C# 14 extension members throughout and cannot be downgraded to earlier TFMs. + +--- + +## Table of Contents + +1. [Installation](#installation) +2. [Quick Start](#quick-start) +3. [Assembly Registry](#assembly-registry) +4. [OpenAPI](#openapi) +5. [Logging](#logging) +6. [MediatR and FluentValidation](#mediatr-and-fluentvalidation) +7. [CORS](#cors) +8. [Resilience Pipelines](#resilience-pipelines) +9. [Controllers](#controllers) +10. [SignalR](#signalr) +11. [OpenTelemetry](#opentelemetry) +12. [Health Checks](#health-checks) +13. [Maintenance Mode](#maintenance-mode) +14. [Utilities](#utilities) + +--- + +## Installation + +```bash +dotnet add package Pandatech.SharedKernel +``` + +--- + +## Quick Start + +A complete `Program.cs` using every major feature: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.LogStartAttempt(); +AssemblyRegistry.Add(typeof(Program).Assembly); + +builder + .ConfigureWithPandaVault() + .AddSerilog(LogBackend.ElasticSearch) + .AddResponseCrafter(NamingConvention.ToSnakeCase) + .AddOpenApi() + .AddMaintenanceMode() + .AddOpenTelemetry() + .AddMinimalApis(AssemblyRegistry.ToArray()) + .AddControllers(AssemblyRegistry.ToArray()) + .AddMediatrWithBehaviors(AssemblyRegistry.ToArray()) + .AddResilienceDefaultPipeline() + .AddDistributedSignalR("localhost:6379", "app_name") + .AddCors() + .AddOutboundLoggingHandler() + .AddHealthChecks(); + +var app = builder.Build(); + +app + .UseRequestLogging() + .UseMaintenanceMode() + .UseResponseCrafter() + .UseCors() + .MapMinimalApis() + .EnsureHealthy() + .MapHealthCheckEndpoints() + .MapPrometheusExporterEndpoints() + .ClearAssemblyRegistry() + .UseOpenApi() + .MapControllers(); + +app.LogStartSuccess(); +app.Run(); +``` + +--- + +## Assembly Registry + +`AssemblyRegistry` is a thread-safe static list used to pass your project's assemblies from the builder phase to the +app phase without repeating `typeof(Program).Assembly` everywhere. + +```csharp +// Add once at startup +AssemblyRegistry.Add(typeof(Program).Assembly); + +// Pass to any method that needs to scan for handlers, validators, or endpoints +builder.AddMediatrWithBehaviors(AssemblyRegistry.ToArray()); + +// Clear after app is built to free memory — the scanning is complete +app.ClearAssemblyRegistry(); +``` + +--- + +## OpenAPI + +Wraps `Microsoft.AspNetCore.OpenApi` with SwaggerUI and Scalar, supporting multiple API documents, custom security +schemes, and enum string descriptions. + +### Registration + +```csharp +builder.AddOpenApi(); +var app = builder.Build(); +app.UseOpenApi(); +``` + +Custom schema transformers can be added via the options callback: + +```csharp +builder.AddOpenApi(options => +{ + options.AddSchemaTransformer(); +}); +``` + +### Configuration + +```json +{ + "OpenApi": { + "DisabledEnvironments": [ + "Production" + ], + "SecuritySchemes": [ + { + "HeaderName": "Authorization", + "Description": "Bearer access token." + }, + { + "HeaderName": "Client-Type", + "Description": "Identifies the client type, e.g. '2'." + } + ], + "Documents": [ + { + "Title": "Admin Panel", + "Description": "Internal administrative endpoints.", + "GroupName": "admin-v1", + "Version": "v1", + "ForExternalUse": false + }, + { + "Title": "Integration", + "Description": "Public integration endpoints.", + "GroupName": "integration-v1", + "Version": "v1", + "ForExternalUse": true + } + ], + "Contact": { + "Name": "Pandatech", + "Url": "https://pandatech.it", + "Email": "info@pandatech.it" + } + } +} +``` + +### UI URLs + +| UI | URL | Notes | +|---------|---------------------------|------------------------------------------| +| Swagger | `/swagger` | All documents | +| Swagger | `/swagger/integration-v1` | External documents only (ForExternalUse) | +| Scalar | `/scalar/admin-v1` | One URL per document | +| Scalar | `/scalar/integration-v1` | One URL per document | + +`ForExternalUse: true` creates a dedicated Swagger URL you can share with external partners while keeping internal +documents private. All documents still appear on the main `/swagger` page. + +--- + +## Logging + +Wraps Serilog with structured output, request/response logging middleware, outbound HTTP logging, and automatic log +cleanup. + +### Registration + +```csharp +// Synchronous sinks — safe for up to ~1000 req/s per pod +builder.AddSerilog(LogBackend.Loki); + +// Asynchronous sinks — better throughput, small risk of losing logs on hard crash +builder.AddSerilog( + logBackend: LogBackend.ElasticSearch, + logAdditionalProperties: new Dictionary + { + ["ServiceName"] = "my-service" + }, + daysToRetain: 14, + asyncSinks: true +); +``` + +### Log Backends + +| Value | Output format | +|-----------------|---------------------------------------------------| +| `None` | Console only, no file output | +| `ElasticSearch` | ECS JSON to file (forward with Filebeat/Logstash) | +| `Loki` | Loki JSON to file (forward with Promtail) | +| `CompactJson` | Compact JSON to file | + +### Environment behavior + +| Environment | Console | File | +|------------------|---------|------| +| Local | Yes | No | +| Development / QA | Yes | Yes | +| Production | No | Yes | + +### Configuration + +```json +{ + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Information", + "System": "Information" + } + } + }, + "RepositoryName": "my-service", + "ConnectionStrings": { + "PersistentStorage": "/persistence" + } +} +``` + +Log files are stored under `{PersistentStorage}/{RepositoryName}/{env}/logs/`. The `LogCleanupHostedService` runs +every 12 hours and deletes files older than `daysToRetain`. + +### Request logging middleware + +```csharp +app.UseRequestLogging(); // logs method, path, status code, elapsed ms, redacted headers/body +``` + +Paths under `/swagger`, `/openapi`, `/above-board`, and `/favicon.ico` are silently skipped. Sensitive header names +(auth, token, cookie, pan, cvv, etc.) and matching JSON properties are redacted automatically. Bodies over 16 KB are +omitted. + +### Outbound logging + +Captures outbound `HttpClient` requests with the same redaction rules: + +```csharp +// Register the handler +builder.AddOutboundLoggingHandler(); + +// Attach to a specific HttpClient +builder.Services + .AddHttpClient("MyClient", c => c.BaseAddress = new Uri("https://example.com")) + .AddOutboundLoggingHandler(); +``` + +### Startup logging + +```csharp +builder.LogStartAttempt(); // prints banner to console at startup +app.LogStartSuccess(); // prints success banner with elapsed init time +``` + +--- + +## MediatR and FluentValidation + +Registers MediatR with a validation pipeline behavior that runs all FluentValidation validators before the handler. +Validation failures throw `BadRequestException` from `Pandatech.ResponseCrafter`. + +### Registration + +```csharp +builder.AddMediatrWithBehaviors(AssemblyRegistry.ToArray()); +``` + +### CQRS interfaces + +```csharp +// Commands +public record CreateUserCommand(string Email) : ICommand; +public class CreateUserHandler : ICommandHandler { ... } + +// Queries +public record GetUserQuery(Guid Id) : IQuery; +public class GetUserHandler : IQueryHandler { ... } +``` + +### FluentValidation extensions + +**String validators** + +```csharp +RuleFor(x => x.Email).IsEmail(); +RuleFor(x => x.Phone).IsPhoneNumber(); // Panda format: (374)91123456 +RuleFor(x => x.Contact).IsEmailOrPhoneNumber(); +RuleFor(x => x.Payload).IsValidJson(); +RuleFor(x => x.Content).IsXssSanitized(); +``` + +**Single file (`IFormFile`)** + +```csharp +RuleFor(x => x.Avatar) + .HasMaxSizeMb(6) + .ExtensionIn(".jpg", ".jpeg", ".png"); +``` + +**File collection (`IFormFileCollection`)** + +```csharp +RuleFor(x => x.Docs) + .MaxCount(10) + .EachHasMaxSizeMb(10) + .EachExtensionIn(CommonFileSets.Documents) + .TotalSizeMaxMb(50); +``` + +**File presets** + +```csharp +CommonFileSets.Images // .jpg .jpeg .png .webp .heic .heif .svg .avif +CommonFileSets.Documents // .pdf .txt .csv .json .xml .yaml .md .docx .xlsx .pptx ... +CommonFileSets.ImagesAndAnimations // Images + .gif +CommonFileSets.ImagesAndDocuments // Images + Documents +CommonFileSets.ImportFiles // .csv .xlsx +``` + +--- + +## CORS + +Development and non-production environments allow all origins. Production restricts to the configured list and +automatically adds both `www` and non-`www` variants. + +### Registration + +```csharp +builder.AddCors(); +app.UseCors(); +``` + +### Production configuration + +```json +{ + "Security": { + "AllowedCorsOrigins": "https://example.com,https://api.example.com" + } +} +``` + +The list accepts comma- or semicolon-separated URLs. Invalid entries are logged and filtered out. + +--- + +## Resilience Pipelines + +Built on Polly via `Microsoft.Extensions.Http.Resilience`. Provides retry, circuit breaker, and timeout policies for +`HttpClient` calls. + +### Options + +**1. Global — applies to all registered HttpClients:** + +```csharp +builder.AddResilienceDefaultPipeline(); +``` + +**2. Per-client:** + +```csharp +builder.Services.AddHttpClient("MyClient") + .AddResilienceDefaultPipeline(); +``` + +**3. Manual — for wrapping arbitrary async calls:** + +```csharp +public class MyService(ResiliencePipelineProvider provider) +{ + public async Task CallAsync() + { + var pipeline = provider.GetDefaultPipeline(); + var result = await pipeline.ExecuteAsync(() => _client.GetAsync("/endpoint")); + } +} +``` + +### Default pipeline policies + +| Policy | Configuration | +|-----------------|--------------------------------------------------------| +| Retry (429) | 5 retries, exponential backoff, respects `Retry-After` | +| Retry (5xx/408) | 7 retries, exponential backoff from 800ms | +| Circuit breaker | Opens at 50% failure rate over 30 s, min 200 requests | +| Timeout | 8 seconds per attempt | + +--- + +## Controllers + +For applications using classic MVC controllers alongside minimal APIs: + +```csharp +builder.AddControllers(AssemblyRegistry.ToArray()); +app.MapControllers(); +``` + +Controller and action names are automatically kebab-cased (`UserProfile` → `user-profile`). + +--- + +## SignalR + +**Local SignalR (single instance):** + +```csharp +builder.AddSignalR(); +``` + +**Distributed SignalR backed by Redis (multi-instance):** + +```csharp +builder.AddDistributedSignalR("localhost:6379", "app_name"); +``` + +Both variants include: + +- `SignalRLoggingHubFilter` — logs hub method calls with redacted arguments and elapsed time +- `SignalRExceptionFilter` — from `Pandatech.ResponseCrafter`, standardizes error responses +- MessagePack protocol for compact binary serialization + +--- + +## OpenTelemetry + +```csharp +builder.AddOpenTelemetry(); +app.MapPrometheusExporterEndpoints(); +``` + +### What is included + +- ASP.NET Core metrics and traces +- HttpClient metrics and traces +- Entity Framework Core traces +- Runtime metrics +- Prometheus scraping endpoint at `/above-board/prometheus` +- Health metrics at `/above-board/prometheus/health` + +### OTLP export + +Set the following in your environment config or as an environment variable to enable OTLP export: + +```json +{ + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317" +} +``` + +--- + +## Health Checks + +```csharp +builder.AddHealthChecks(); + +app.EnsureHealthy(); // runs health checks at startup; throws if anything is unhealthy +app.MapHealthCheckEndpoints(); // registers /above-board/ping and /above-board/health +``` + +`EnsureHealthy` skips MassTransit bus checks during startup (those take time to connect). The ping endpoint returns +`"pong"` as plain text. The health endpoint returns the full AspNetCore.HealthChecks.UI JSON format. + +Additional health check registrations follow the standard `builder.Services.AddHealthChecks().Add...()` pattern — the +library does not wrap those. + +--- + +## Maintenance Mode + +Three-mode global switch. Requires `Pandatech.DistributedCache` to synchronize state across instances. + +| Mode | Effect | +|---------------------|--------------------------------------------------------------------| +| `Disabled` | Normal operation | +| `EnabledForClients` | All routes blocked except `/api/admin/*` and `/hub/admin/*` | +| `EnabledForAll` | All routes blocked except `/above-board/*` and `OPTIONS` preflight | + +### Registration + +```csharp +builder.AddMaintenanceMode(); +app.UseMaintenanceMode(); // place before UseResponseCrafter and UseCors +``` + +### Controlling maintenance mode + +Map the built-in endpoint and protect it with your own authorization: + +```csharp +app.MapMaintenanceEndpoint(); +// PUT /above-board/maintenance body: { "mode": 1 } +``` + +Or protect with a shared secret query parameter (useful before auth is in place): + +```csharp +app.MapMaintenanceEndpoint(querySecret: "my-secret"); +// PUT /above-board/maintenance?secret=my-secret +``` + +Programmatic control from application code: + +```csharp +public class AdminService(MaintenanceState state) +{ + public Task EnableMaintenanceAsync(CancellationToken ct) + => state.SetModeAsync(MaintenanceMode.EnabledForClients, ct); +} +``` + +--- + +## Utilities + +### ValidationHelper + +Static regex-based validators with a 50ms timeout per expression. + +```csharp +ValidationHelper.IsEmail("user@example.com"); +ValidationHelper.IsUri("https://example.com", allowNonSecure: false); +ValidationHelper.IsGuid("12345678-1234-1234-1234-123456789012"); +ValidationHelper.IsPandaFormattedPhoneNumber("(374)91123456"); +ValidationHelper.IsArmeniaSocialSecurityNumber("1234567890"); +ValidationHelper.IsArmeniaIdCard("123456789"); +ValidationHelper.IsArmeniaPassportNumber("AB1234567"); +ValidationHelper.IsArmeniaTaxCode("12345678"); +ValidationHelper.IsArmeniaStateRegistryNumber("123.456.78901"); +ValidationHelper.IsIPv4("192.168.1.1"); +ValidationHelper.IsIPv6("2001:db8::1"); +ValidationHelper.IsIpAddress("192.168.1.1"); +ValidationHelper.IsJson("{\"key\":\"value\"}"); +ValidationHelper.IsCreditCardNumber("4111111111111111"); +ValidationHelper.IsUsSocialSecurityNumber("123-45-6789"); +ValidationHelper.IsUsername("user123"); +``` + +### LanguageIsoCodeHelper + +```csharp +LanguageIsoCodeHelper.IsValidLanguageCode("hy-AM"); // true +LanguageIsoCodeHelper.GetName("hy-AM"); // "Armenian (Armenia)" +LanguageIsoCodeHelper.GetCode("Armenian (Armenia)"); // "hy-AM" +``` + +Covers 170+ language-region combinations. The lookup table is initialized once at startup. + +### PhoneUtil + +Normalizes Armenian phone numbers to `+374XXXXXXXX` format from a variety of input formats: + +```csharp +PhoneUtil.TryFormatArmenianMsisdn("(374)91123456", out var formatted); // "+37491123456" +PhoneUtil.TryFormatArmenianMsisdn("+374 91 12 34 56", out var formatted); // "+37491123456" +PhoneUtil.TryFormatArmenianMsisdn("091123456", out var formatted); // "+37491123456" +``` + +Returns `false` and the original input if the number cannot be parsed as an Armenian MSISDN. + +### UrlBuilder + +```csharp +var url = UrlBuilder.Create("https://api.example.com/users") + .AddParameter("page", "1") + .AddParameter("size", "20") + .Build(); +// https://api.example.com/users?page=1&size=20 +``` + +### TimeZone extensions + +```csharp +// Set once at startup from appsettings DefaultTimeZone +builder.MapDefaultTimeZone(); + +// Convert any DateTime to the configured zone +var local = someUtcDateTime.ToDefaultTimeZone(); +``` + +### IHostEnvironment extensions + +```csharp +env.IsLocal(); +env.IsQa(); +env.IsLocalOrDevelopment(); +env.IsLocalOrDevelopmentOrQa(); +env.GetShortEnvironmentName(); // "local" | "dev" | "qa" | "staging" | "" +``` + +### HttpContext extensions + +```csharp +// Mark a response as private (adds X-Private-Endpoint: 1 header) +context.MarkAsPrivateEndpoint(); +``` + +### Collection extensions + +```csharp +// IEnumerable / IQueryable +var filtered = items.WhereIf(condition, x => x.IsActive); + +// In operator +if (status.In(Status.Active, Status.Pending)) { ... } +``` + +### Dictionary extensions (zero-allocation via CollectionsMarshal) + +```csharp +dict.GetOrAdd(key, defaultValue); +dict.TryUpdate(key, newValue); +``` + +### JsonConverters + +| Converter | Behavior | +|---------------------------|------------------------------------------------------------| +| `EnumConverterFactory` | Accepts enum by name or integer; serializes as name string | +| `CustomDateOnlyConverter` | Parses and writes `DateOnly` in `dd-MM-yyyy` format | + +Register via `JsonSerializerOptions.Converters` or your `ResponseCrafter` setup. + +### MethodTimingStatistics + +Development-only benchmarking helper. Not for production use (marked with `#warning`). + +```csharp +var ts = Stopwatch.GetTimestamp(); +DoWork(); +MethodTimingStatistics.RecordExecution("DoWork", ts); +MethodTimingStatistics.LogAll(logger); +``` + +--- + +## PandaVault + +```csharp +builder.ConfigureWithPandaVault(); +``` + +Loads secrets from PandaVault on all non-Local environments. On Local, the call is a no-op so local `appsettings.json` +is used unchanged. + +--- + +## Related Packages + +| Package | Purpose | +|------------------------------------|-----------------------------------------------------------| +| `Pandatech.ResponseCrafter` | Consistent API error responses | +| `Pandatech.DistributedCache` | Redis-backed hybrid cache (required for maintenance mode) | +| `Pandatech.Crypto` | Cryptographic utilities | +| `Pandatech.FluentMinimalApiMapper` | Minimal API endpoint mapping | + +--- + +## License + +MIT \ No newline at end of file diff --git a/Readme.md b/Readme.md deleted file mode 100644 index a6029f1..0000000 --- a/Readme.md +++ /dev/null @@ -1,796 +0,0 @@ -# Pandatech.SharedKernel - -Welcome to the `Pandatech.SharedKernel` NuGet package - a centralized library designed to streamline development across -all PandaTech projects. This package consolidates shared configurations, utilities, and extensions into a single, -reusable resource. - -Although this package is primarily intended for internal use, it is publicly available for anyone who may find it -useful. We recommend forking or copying the classes in this repository and creating your own package to suit your needs. - -By leveraging this shared kernel, we aim to: - -- Reduce the amount of boilerplate code required to start a new project. -- Ensure consistency across all PandaTech projects. -- Simplify the process of updating shared configurations and utilities. - -## Scope - -This package currently supports: - -- **OpenAPI Configuration** with SwaggerUI and Scalar. -- **Logging** with Serilog (including ECS, Loki and compact JSON file output, plus automatic log cleanup). -- **MediatR and FluentValidation** configurations. -- **Cors Configuration** with easy configuration options. -- **Resilience Pipelines** for `HttpClient` operations. -- **Controller Extensions** for mapping old-style MVC controllers. -- **SignalR Extensions** for adding simple SignalR or distributed SignalR backed with Redis. -- **OpenTelemetry**: Metrics, traces, and logs with Prometheus support. -- **Health Checks**: Startup validation and endpoints for monitoring. -- **ValidationHelper**: A collection of regex-based validators for common data formats. -- **Maintenance Mode**: Global switch with three modes (`Disabled`, `EnabledForClients`, `EnabledForAll`); clients = all - routes except `/api/admin/*`. -- Various **Extensions and Utilities**, including enumerable, string, dictionary and queryable extensions. - -## Prerequisites - -- .NET 9.0 SDK or higher - -## Installation - -To install the `Pandatech.SharedKernel` package, use the following command: - -```bash -dotnet add package Pandatech.SharedKernel -``` - -Alternatively, you can add it via the NuGet Package Manager in Visual Studio, VS Code or Rider. - -## Full SharedKernel Demo - -This section demonstrates how to use the `Pandatech.SharedKernel` package in a fully functional application. It includes -examples of: - -- Comprehensive `appsettings.json` configurations. -- Environment-specific settings in `appsettings.{Environment}.json`. -- A complete `Program.cs` implementation with all major features integrated. - -Follow this example to set up your project with all the features provided by this library. - -### appsettings.json - -```json -{ - "OpenApi": { - "DisabledEnvironments": [ - "Production" - ], - "SecuritySchemes": [ - { - "HeaderName": "Client-Type", - "Description": "Specifies the client type, e.g., '2'." - }, - { - "HeaderName": "Authorization", - "Description": "Access token for the API." - } - ], - "Documents": [ - { - "Title": "Administrative Panel Partners", - "Description": "This document describes the API endpoints for the Administrative Panel Partners.", - "GroupName": "admin-v1", - "Version": "v1", - "ForExternalUse": false - }, - { - "Title": "Integration", - "Description": "Integration API Endpoints", - "GroupName": "integration-v1", - "Version": "v1", - "ForExternalUse": true - } - ], - "Contact": { - "Name": "Pandatech", - "Url": "https://pandatech.it", - "Email": "info@pandatech.it" - } - } -} -``` - -### appsettings.{Environment}.json - -```json -{ - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Information", - "System": "Information" - } - } - }, - "ResponseCrafterVisibility": "Private", - "DefaultTimeZone": "Caucasus Standard Time", - "RepositoryName": "be-lib-sharedkernel", - "ConnectionStrings": { - "Redis": "localhost:6379", - "PersistentStorage": "/persistence" - } -} -``` - -### Program.cs - -```csharp -var builder = WebApplication.CreateBuilder(args); - -builder.LogStartAttempt(); -AssemblyRegistry.Add(typeof(Program).Assembly); - -builder - .ConfigureWithPandaVault() - .AddSerilog(LogBackend.ElasticSearch) - .AddResponseCrafter(NamingConvention.ToSnakeCase) - .AddOpenApi() - .AddMaintenanceMode() - .AddOpenTelemetry() - .AddMinimalApis(AssemblyRegistry.ToArray()) - .AddControllers(AssemblyRegistry.ToArray()) - .AddMediatrWithBehaviors(AssemblyRegistry.ToArray()) - .AddResilienceDefaultPipeline() - .AddDistributedSignalR("localhost:6379", "app_name") // or .AddSignalR() - .AddDistributedCache(o => - { - o.RedisConnectionString = "localhost:6379"; - o.ChannelPrefix = "app_name"; - }) - .AddMassTransit(AssemblyRegistry.ToArray()) - .AddFileExporter(AssemblyRegistry.ToArray()) - .MapDefaultTimeZone() - .AddCors() - .AddOutboundLoggingHandler() - .AddHealthChecks(); - - -var app = builder.Build(); - -app - .UseRequestLogging() - .UseMaintenanceMode() //(place early) - .UseResponseCrafter() - .UseCors() - .MapMinimalApis() - .EnsureHealthy() - .MapHealthCheckEndpoints() - .MapPrometheusExporterEndpoints() - .ClearAssemblyRegistry() - .UseOpenApi() - .MapControllers(); - -app.LogStartSuccess(); -app.Run(); -``` - -For a deeper dive into each feature, please refer to the following sections. - -## OpenAPI - -`Microsoft.AspNetCore.OpenApi` is the new standard for creating OpenAPI JSON files. We have adopted this library instead -of Swashbuckle for generating OpenAPI definitions. Along with this new library, we have integrated `SwaggerUI` and -`Scalar` to provide user-friendly interfaces in addition to the JSON files. - -### Key Features - -- **Multiple API Documents:** Easily define and organize multiple API documentation groups. -- **Enum String Values:** Enum string values are automatically displayed in the documentation, simplifying integration - for external partners. -- **Security Schemes:** Configure security headers directly in your OpenAPI settings. - -### Adding OpenAPI to Your Project - -To enable OpenAPI in your project, add the following code: - -```csharp -var builder = WebApplication.CreateBuilder(args); -builder.AddOpenApi(); -var app = builder.Build(); -app.UseOpenApi(); -app.Run(); -``` - -You can also customize the `AddOpenApi` method with options: - -```csharp -builder.AddOpenApi(options => -{ - options.AddSchemaTransformer(); -}); -``` - -### Configuration - -Add the following configuration to your `appsettings.json` file: - -```json -{ - "OpenApi": { - "DisabledEnvironments": [ - "Production" - ], - "SecuritySchemes": [ - { - "HeaderName": "Authorization", - "Description": "Access token for the API." - } - ], - "Documents": [ - { - "Title": "Admin Panel API", - "Description": "API for administrative functions.", - "GroupName": "admin-v1", - "Version": "v1", - "ForExternalUse": false - }, - { - "Title": "Integration", - "Description": "Integration API Endpoints", - "GroupName": "integration-v1", - "Version": "v1", - "ForExternalUse": true - } - ], - "Contact": { - "Name": "Pandatech", - "Url": "https://pandatech.it", - "Email": "info@pandatech.it" - } - } -} -``` - -### Notes - -- **For External Use:** If you set `ForExternalUse: true` for a document, it will be available both within the regular - SwaggerUI and a separate SwaggerUI instance. This allows you to provide a dedicated URL to external partners while - keeping internal documents private. -- **Scalar UI Limitations:** Scalar currently does not support multiple documents within a single URL. Consequently, all - documents in Scalar will be separated into individual URLs. Support for multiple documents is expected in future - Scalar updates. - -### Example URLs - -Based on the above configuration, the UI will be accessible at the following URLs: - -- **Swagger (all documents):** [http://localhost/swagger](http://localhost/swagger) -- **Swagger (external document only): - ** [http://localhost/swagger/integration-v1](http://localhost/swagger/integration-v1) -- **Scalar (admin document):** [http://localhost/scalar/admin-v1](http://localhost/scalar/admin-v1) -- **Scalar (integration document):** [http://localhost/scalar/integration-v1](http://localhost/scalar/integration-v1) - -## Logging - -### Key Features - -- **Serilog Integration:** Simplified setup for structured logging using Serilog. -- **Log Backend Option** Choose between: - - `LogBackend.None` (disables file logging completely), - - `LogBackend.ElasticSearch` (ECS formatter to file), or - - `LogBackend.Loki` (Loki formatter to file), or - - `LogBackend.CompactJson` (compact JSON format to file). -- **Environment-Specific Configuration:** - - **Local:** Only logs to console even if you choose `LogBackend.Any`. - - **Production:** Logs to file (in ECS or Loki format depending on the backend). - - **Other Environments:** Logs to both console and file. -- **Automatic Log Cleanup:** Log files are automatically cleaned up based on the configured retention period. -- **Log File Location:** Logs are stored in a persistent path defined in your configuration, organized by repository - name and environment, under the `logs` directory. -- **Filtering:** Excludes unwanted logs from Hangfire Dashboard, Swagger, outbox DB commands, and MassTransit health - checks. -- **Request Logging:** Middleware that logs incoming requests and outgoing responses while redacting sensitive - information and large payloads. -- **Outbound Logging Handler:** For capturing outbound `HttpClient` requests (including headers and bodies) with the - same redaction rules. - -### Adding Logging to Your Project - -Use the `AddSerilog` extension when building your `WebApplicationBuilder`. You can specify: - -- `logBackend`: One of `None`, `ElasticSearch` (ECS file format), `Loki` (Loki JSON file format) or `CompactJson`. -- `daysToRetain`: Number of days to keep log files. Older files are automatically removed by the background hosted - service. - -In your middleware pipeline, add the request and response logging middleware: - -```csharp -// 1) Synchronous logging with 7-day retention (default). -builder.AddSerilog(LogBackend.Loki); - -// 2) Asynchronous logging with 14-day retention and extra properties. -// Suitable for high-load (~1000+ RPS per pod) scenarios where slight risk of log loss is acceptable -// in exchange for better performance. -builder.AddSerilog( - logBackend: LogBackend.Loki, - logAdditionalProperties: new Dictionary - { - ["ServiceName"] = "MyApp", - ["Environment"] = "Staging" - }, - daysToRetain: 14, - asyncSinks: true -); -``` - -> - **Asynchronous Sinks (asyncSinks: true):** Recommended for very high-traffic environments (e.g., 1000+ requests per - second per pod) where performance is critical and the possibility of losing a small amount of log data (e.g., on - sudden process termination) is acceptable.

->- **Synchronous Sinks (asyncSinks: false):** Recommended if you can handle up to ~1000 requests per second per pod and - must - retain every log entry without fail. This might incur slightly more overhead but ensures maximum reliability. - -Configure minimal Serilog settings in your environment JSON files as needed, for example in -`appsettings.{Environment}.json`: - -```json -{ - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Information", - "System": "Information" - } - } - }, - "RepositoryName": "be-lib-sharedkernel", - "ConnectionStrings": { - "PersistentStorage": "/persistence" - } -} -``` - -### Log Cleanup - -When you call `builder.AddSerilog(..., daysToRetain: X)`, a `LogCleanupHostedService` is automatically registered. This -hosted service runs periodically to delete log files older than the specified retention period. - -### Usage Notes - -- **No Direct Sinks to External Systems:** By default, logs are written to local files with ECS or Loki JSON format. You - can - later push these files to external systems (e.g., via Filebeat, Logstash, Promtail, or any specialized agent). -- **Optional Enrichment:** You can pass a `Dictionary` to `AddSerilog` to add extra log properties - globally: - ```csharp - builder.AddSerilog( - logBackend: LogBackend.Loki, - logAdditionalProperties: new Dictionary - { - {"ServiceName", "MyService"}, - {"ServiceVersion", "1.0.0"} - } - ); - ``` - -### Startup Logging - -The package provides methods to log application startup events: - -- `LogStartAttempt()`: Logs when the application start is attempted. -- `LogStartSuccess()`: Logs when the application has successfully started, including the initialization time. -- `LogModuleRegistrationSuccess(moduleName)`: Logs successful registration of a module. -- `LogModuleUseSuccess(moduleName)`: Logs successful usage of a module. - -Example: - -```csharp -var builder = WebApplication.CreateBuilder(args); -builder.LogStartAttempt(); -// Configure services -var app = builder.Build(); -// Configure middleware -app.UseRequestLogging(); -// Other middleware -app.LogStartSuccess(); -app.Run(); -``` - -### Outbound Logging with HttpClient - -In addition to the `RequestLoggingMiddleware` for inbound requests, you can now log **outbound** HTTP calls via an -`OutboundLoggingHandler`. This handler captures request and response data (including headers and bodies), automatically -redacting sensitive information (e.g., passwords, tokens). - -#### Usage - -1. **Register the handler** in your `WebApplicationBuilder`: - ```csharp - builder.AddOutboundLoggingHandler(); - ``` -2. **Attach** the handler to any HttpClient registration: - ```csharp - builder.Services - .AddHttpClient("RandomApiClient", client => - { - client.BaseAddress = new Uri("http://localhost"); - }) - .AddOutboundLoggingHandler(); - ``` -3. **Check logs:** Outbound requests and responses are now logged with redacted headers and bodies, just like inbound - traffic. - -> Note: The same redaction rules apply to inbound and outbound calls. Update RedactionHelper if you need to modify the -> behavior (e.g., adding new sensitive keywords). - -## MediatR and FluentValidation Integration - -### Key Features - -- **MediatR Integration:** Simplifies the implementation of the Mediator pattern for handling commands and queries. -- **Custom Interfaces for CQRS:** Provides ICommand and IQuery interfaces to facilitate the Command Query Responsibility - Segregation (CQRS) pattern. -- **Validation Behaviors:** Automatically validates requests using FluentValidation before they reach the handler. -- **FluentValidation Extensions:** Includes custom validators for common scenarios like file size, file type, JSON - validity, - and XSS sanitization. - -### Adding MediatR to Your Project - -To enable MediatR with validation behaviors in your project, add the following code: - -```csharp -var builder = WebApplication.CreateBuilder(args); -builder.AddMediatrWithBehaviors([typeof(Program).Assembly]); -``` - -This extension method registers MediatR and adds custom pipeline behaviors for validation. It scans the specified -assemblies for handlers and validators. - -### How It Works - -**Custom Interfaces for CQRS** -The package provides custom interfaces to distinguish between commands and queries, aligning with the CQRS pattern: - -- Commands: - - `ICommand` and `ICommand` - - Handled by `ICommandHandler` and `ICommandHandler` -- Queries: - - `IQuery` and `IQuery` - - Handled by `IQueryHandler` and `IQueryHandler` - -These interfaces help in organizing your application logic and can be extended in the future, for example, to route read -requests to replicas and write requests to a primary database. - -### Validation Behaviors - -Two custom pipeline behaviors are added to MediatR: - -- `ValidationBehaviorWithResponse`: Used for requests that expect a response. -- `ValidationBehaviorWithoutResponse`: Used for requests that do not expect a response. - -These behaviors automatically validate the incoming requests using FluentValidation validators before they reach the -handler. If validation fails, a `BadRequestException` is thrown with the validation errors. -> `BadRequestException` is from Pandatech.ResponseCrafter NuGet package - -### FluentValidation Extensions - -We ship lightweight validators and presets for common scenarios, including file uploads and string checks. -All file rules use name/extension checks only (simple + fast). Deep validation still happens inside your storage layer. - -**File upload validators** - -**Single file (`IFormFile`)** - -```csharp -RuleFor(x => x.Avatar) - .HasMaxSizeMb(6) // size cap in MB - .ExtensionIn(".jpg", ".jpeg", ".png"); // or use a preset set below - -``` - -**File collection (`IFormFileCollection`)** - -```csharp -RuleFor(x => x.Docs) - .MaxCount(10) // number of files - .EachHasMaxSizeMb(10) // per-file size cap (MB) - .EachExtensionIn(CommonFileSets.Documents) - .TotalSizeMaxMb(50); // sum of all files (MB) -``` - -**Presets** - -```csharp -using SharedKernel.ValidatorAndMediatR.Validators.Files; - -CommonFileSets.Images // .jpg, .jpeg, .png, .webp, .heic, .heif, .svg, .avif -CommonFileSets.Documents // .pdf, .txt, .csv, .json, .xml, .yaml, .yml, .md, .docx, .xlsx, .pptx, .odt, .ods, .odp -CommonFileSets.ImagesAndAnimations // Images + .gif -CommonFileSets.ImagesAndDocuments // Images + Documents -``` - -**String validators** - -```csharp -RuleFor(x => x.Email).IsEmail(); -RuleFor(x => x.Phone).IsPhoneNumber(); -RuleFor(x => x.Contact).IsEmailOrPhoneNumber(); // alias: IsPhoneNumberOrEmail -RuleFor(x => x.PayloadJson).IsValidJson(); -RuleFor(x => x.Content).IsXssSanitized(); -``` - -## Cors - -### Key Features - -- **Non-Production Environment:** Allows all origins for ease of development and testing. -- **Production Environment** - - Restricts origins to a specific list defined in the configurations. - - Automatically handles `www` and non-`www` versions of allowed origins. - - Supports credentials, all methods and headers, and preflight caching. - -### Adding Cors to Your Project - -To enable Cors in your project, add the following code: - -```csharp -var builder = WebApplication.CreateBuilder(args); -builder.AddCors(); -var app = builder.Build(); -app.UseCors(); -app.Run(); -``` - -### Configuration - -Add following to your `appsetings.production.json` file: - -```json -{ - "Security": { - "AllowedCorsOrigins": "https://example.com,https://api.example.com" - } -} -``` - -- AllowedCorsOrigins: A comma- or semicolon-separated list of allowed origins. Invalid URLs are automatically filtered - out. - -## Resilience Pipelines - -### Key Features - -- **Default Resilience Pipeline:** Provides a pre-configured resilience pipeline with retry, circuit breaker, and - timeout - policies. -- **Polly Integration:** Utilizes the Microsoft.Extensions.Http.Resilience library which is built on Polly library for - implementing advanced resilience strategies. -- **Flexible Configuration:** Can be applied globally via WebApplicationBuilder or locally within specific HTTP client - configurations. -- **HTTP Client Resilience:** Enhances the reliability of HTTP client calls by handling transient faults and network - issues. - -### Adding Resilience Pipelines to Your Project - -To enable the default resilience pipeline in your project, you have 3 options: - -1. Global Configuration via `WebApplicationBuilder` - This method applies the resilience pipeline to all HTTP clients registered in your application. - ```csharp - var builder = WebApplication.CreateBuilder(args); - builder.AddResilienceDefaultPipeline(); - ``` -2. Local Configuration within HTTP Client Registration - This method applies the resilience pipeline to a specific HTTP client registration. - ```csharp - builder.Services.AddHttpClient("MyHttpClient") - .AddResilienceDefaultPipeline(); - ``` -3. Configuration within HttpClientService.cs - ```csharp - public class MyHttpClientService - { - private readonly ResiliencePipelineProvider _resiliencePipelineProvider; - private readonly HttpClient _httpClient; - - public MyHttpClientService(ResiliencePipelineProvider resiliencePipelineProvider, HttpClient httpClient) - { - _resiliencePipelineProvider = resiliencePipelineProvider; - _httpClient = httpClient; - } - - public async Task FooAsync() - { - var pipeline = _resiliencePipelineProvider.GetDefaultPipeline(); - var response = await pipeline.ExecuteAsync(() => _httpClient.GetAsync("https://example.com")); - } - } - ``` - -### How It Works - -The default resilience pipeline includes the following policies: - -- **Retry Policy for 429 (Too Many Requests):** Retries the request up to 5 times with an exponential backoff, - respecting - the `Retry-After` header if present. -- **Retry Policy for Network Errors and Timeouts:** Retries network-related failures up to 7 times with an exponential - backoff. -- **Circuit Breaker Policy:** Breaks the circuit when the failure ratio exceeds 50% within a 30-second sampling - duration, - with a minimum throughput of 200 requests. -- **Timeout Policy:** Times out requests that take longer than 8 seconds. - -## Controller Extensions - -For mapping old style MVC controllers, use `builder.AddControllers()`. -The `AddControllers()` method can also accept assembly names as parameters to scan for controllers. -The `MapControllers()` method maps the controllers to the application. - -Example: - -```csharp -var builder = WebApplication.CreateBuilder(args); -builder.AddControllers([typeof(Program).Assembly]); -var app = builder.Build(); -app.MapControllers(); -app.Run(); -``` - -## Telemetry Integration - -Integrate OpenTelemetry for observability, including metrics, traces, and logging: - -1. Setup: - ```csharp - var builder = WebApplication.CreateBuilder(args); - builder.AddOpenTelemetry(); - var app = builder.Build(); - app.MapPrometheusExporterEndpoints(); - app.Run(); - ``` -2. Prometheus Endpoints: - - Metrics: `url/above-board/prometheus` - - Health Metrics: `url/above-board/prometheus/health` - -3. OTLP Configuration: - To configure the OTLP exporter, ensure the following entries are present in your appsettings{Environment}.json or as - environment variables: - ```json - { - "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317" - } - ``` -4. Included Features: - - ASP.NET Core metrics - - HTTP client telemetry - - Distributed tracing - - Logging - - Prometheus exporter - - OTLP exporter - - EF Core telemetry - -## Maintenance Mode - -- **Modes** - - `Disabled`: normal operation. - - `EnabledForClients`: only `/api/admin/*` or `/hub/admin/*` allowed - - `EnabledForAll`: everything blocked except `/above-board/*` and `OPTIONS`. -- **Security**: use your own auth (recommended). If you don’t have auth yet, you can pass a shared secret to - `MapMaintenanceEndpoint(basePath, querySecret)`. - -> Currently, this feature requires a Pandatech.DistributedCache to work correctly. - -## HealthChecks - -- **Startup Validation:** `app.EnsureHealthy()` performs a health check at startup and terminates the application if it - is not healthy. -- **Endpoints Mapping:** `app.MapHealthCheckEndpoints()` maps default health check endpoints to the application. -- **Mapped Endpoints:** - - Ping Endpoint: `url/above-board/ping` - - Health Check Endpoint: `url/above-board/health` - -Example: - -```csharp -var app = builder.Build(); - -app.EnsureHealthy(); // Startup validation -app.MapHealthCheckEndpoints(); // Map health check routes - -app.Run(); -``` - -## ValidationHelper - -The `ValidationHelper` class is a highly performant and robust C# class designed to simplify complex regex validations -for -various data formats. With 100% test coverage and a focus on security through a 50ms regex execution timeout, it's an -ideal solution for applications requiring reliable and efficient data validation. - -```csharp -using Pandatech.RegexBox; - -// URI validation -bool isValidUri = ValidationHelper.IsUri("http://example.com", allowNonSecure: false); - -// US Social Security Number validation -bool isValidSsnUs = ValidationHelper.IsUsSocialSecurityNumber("123-45-6789"); - -// Email validation -bool isValidEmail = ValidationHelper.IsEmail("user@example.com"); - -// Username validation -bool isValidUsername = ValidationHelper.IsUsername("user123"); - -// Armenian Social Security Number validation -bool isValidSsnAm = ValidationHelper.IsArmeniaSocialSecurityNumber("12345678912"); - -//ArmenianIDCard validation -bool isValidArmenianIdCard = ValidationHelper.IsArmeniaIdCard("AN1234567"); - -// Armenian Passport validation -bool isValidArmenianPassport = ValidationHelper.IsArmeniaPassport("AN1234567"); - -// Armenian Tax code validation -bool isValidArmenianTaxCode = ValidationHelper.IsArmeniaTaxCode("12345678"); - -// Panda Formatted Phone Number validation -bool isValidPhoneNumber = ValidationHelper.IsPandaFormattedPhoneNumber("(374)94810553"); - -// Armenian State Registration Number validation -bool isValidArmenianStateRegistrationNumber = ValidationHelper.IsArmeniaStateRegistryNumber("123.456.78"); - -// Panda formatted phone number validation - -bool isValidPandaFormattedPhoneNumber = ValidationHelper.IsPandaFormattedPhoneNumber("(374)94810553"); - -// Guid validation -bool isValidGuid = ValidationHelper.IsGuid("12345678-1234-1234-1234-123456789012"); - -// IPv4 validation -bool isValidIpv4 = ValidationHelper.IsIPv4("192.168.1.1"); - -// IPv6 validation -bool isValidIpv6 = ValidationHelper.IsIPv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); - -// Any IP validation -bool isValidIp = ValidationHelper.IsIpAddress("192.168.1.1"); - -// Json validation -bool isValidJson = ValidationHelper.IsJson("{\"name\":\"John\", \"age\":30}"); - -// and many more... -``` - -## Additional Extensions and NuGet Packages - -This package includes various extensions and utilities to aid development: - -- **Enumerable Extensions:** Additional LINQ methods for collections. -- **Host Environment Extensions:** Methods to simplify environment checks (e.g., `IsLocal()`, `IsQa()`). -- **Queryable Extensions:** Extensions for IQueryable, such as conditional `WhereIf`. -- **Dictionary Extensions:** Utility methods for dictionary manipulation in a performant way like `GetOrAdd` and - `TryUpdate`. -- **String Extensions:** Utility methods for string manipulation. -- **Time Zone Extensions:** Methods to handle default time zones within your application. Use `.MapDefaultTimeZone()`, - which - retrieves DefaultTimeZone from `appsettings.json` and sets it as the default time zone. -- **UrlBuilder:** A utility for building URLs with query parameters. -- **Language ISO Code Helper:** Validate, query, and retrieve information about ISO language codes. -- **PhoneUtil class** Utility class for phone number formatting. - -### Related NuGet Packages - -- **Pandatech.Crypto:** Provides cryptographic utilities. -- **Pandatech.FluentMinimalApiMapper:** Simplifies mapping in minimal APIs. -- **Pandatech.ResponseCrafter:** A utility for crafting consistent API responses. -- **Pandatech.DistributedCache:** A distributed cache provider for Redis. -- **PandaTech.FluentImporter:** Fluent API for importing data. -- **Pandatech.FileExporter:** A utility for exporting files. - -## License - -MIT License diff --git a/SharedKernel.Demo/SharedKernel.Demo.csproj b/SharedKernel.Demo/SharedKernel.Demo.csproj index 14a82dc..c9ccca4 100644 --- a/SharedKernel.Demo/SharedKernel.Demo.csproj +++ b/SharedKernel.Demo/SharedKernel.Demo.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/SharedKernel/Helpers/LanguageIsoCodeHelper.cs b/src/SharedKernel/Helpers/LanguageIsoCodeHelper.cs index 1ca531c..c6014a7 100644 --- a/src/SharedKernel/Helpers/LanguageIsoCodeHelper.cs +++ b/src/SharedKernel/Helpers/LanguageIsoCodeHelper.cs @@ -2,593 +2,580 @@ namespace SharedKernel.Helpers; public static class LanguageIsoCodeHelper { - private static Dictionary AvailableLanguages => - new() - { - { - "en-US", "English (United States)" - }, - { - "en-CA", "English (Canada)" - }, - { - "ar-BH", "Arabic (Bahrain)" - }, - { - "ar-KW", "Arabic (Kuwait)" - }, - { - "ar-LB", "Arabic (Lebanon)" - }, - { - "ar-OM", "Arabic (Oman)" - }, - { - "ar-QA", "Arabic (Qatar)" - }, - { - "ar-SA", "Arabic (Saudi Arabia)" - }, - { - "ar-AE", "Arabic (UAE)" - }, - { - "bs-BA", "Bosnian (Bosnia and Herzegovina)" - }, - { - "bg-BG", "Bulgarian (Bulgaria)" - }, - { - "ca-ES", "Catalan (Spain)" - }, - { - "hr-HR", "Croatian (Croatia)" - }, - { - "cs-CZ", "Czech (Czech Republic)" - }, - { - "da-DK", "Danish (Denmark)" - }, - { - "de-AT", "German (Austria)" - }, - { - "de-CH", "German (Switzerland)" - }, - { - "de-DE", "German (Germany)" - }, - { - "de-LU", "German (Luxembourg)" - }, - { - "el-GR", "Greek (Greece)" - }, - { - "en-AE", "English (UAE)" - }, - { - "en-AR", "English (Argentina)" - }, - { - "en-AU", "English (Australia)" - }, - { - "en-AT", "English (Austria)" - }, - { - "en-BG", "English (Bulgaria)" - }, - { - "en-BH", "English (Bahrain)" - }, - { - "en-BR", "English (Brazil)" - }, - { - "en-CL", "English (Chile)" - }, - { - "en-CO", "English (Colombia)" - }, - { - "en-HR", "English (Croatia)" - }, - { - "en-CY", "English (Cyprus)" - }, - { - "en-CZ", "English (Czech Republic)" - }, - { - "en-DK", "English (Denmark)" - }, - { - "en-FI", "English (Finland)" - }, - { - "en-DE", "English (Germany)" - }, - { - "en-GB", "English (Great Britain)" - }, - { - "en-GR", "English (Greece)" - }, - { - "en-HK", "English (Hong Kong)" - }, - { - "en-HU", "English (Hungary)" - }, - { - "en-IS", "English (Iceland)" - }, - { - "en-IN", "English (India)" - }, - { - "en-ID", "English (Indonesia)" - }, - { - "en-IE", "English (Ireland)" - }, - { - "en-IL", "English (Israel)" - }, - { - "en-KW", "English (Kuwait)" - }, - { - "en-LV", "English (Latvia)" - }, - { - "en-LB", "English (Lebanon)" - }, - { - "en-MY", "English (Malaysia)" - }, - { - "en-MT", "English (Malta)" - }, - { - "en-MX", "English (Mexico)" - }, - { - "en-NL", "English (Netherlands)" - }, - { - "en-NZ", "English (New Zealand)" - }, - { - "en-NO", "English (Norway)" - }, - { - "en-OM", "English (Oman)" - }, - { - "en-PE", "English (Peru)" - }, - { - "en-PL", "English (Poland)" - }, - { - "en-QA", "English (Qatar)" - }, - { - "en-RO", "English (Romania)" - }, - { - "en-SA", "English (Saudi Arabia)" - }, - { - "en-SE", "English (Sweden)" - }, - { - "en-SG", "English (Singapore)" - }, - { - "en-SK", "English (Slovakia)" - }, - { - "en-SI", "English (Slovenia)" - }, - { - "en-TW", "English (Taiwan)" - }, - { - "en-TH", "English (Thailand)" - }, - { - "en-TR", "English (Turkey)" - }, - { - "en-ZA", "English (South Africa)" - }, - { - "et-EE", "Estonian (Estonia)" - }, - { - "fi-FI", "Finnish (Finland)" - }, - { - "fr-BE", "French (Belgium)" - }, - { - "fr-CA", "French (Canada)" - }, - { - "fr-FR", "French (France)" - }, - { - "fr-LU", "French (Luxembourg)" - }, - { - "fr-CH", "French (Switzerland)" - }, - { - "he-IL", "Hebrew (Israel)" - }, - { - "hi-IN", "Hindi (India)" - }, - { - "hu-HU", "Hungarian (Hungary)" - }, - { - "is-IS", "Icelandic (Iceland)" - }, - { - "id-ID", "Indonesian (Indonesia)" - }, - { - "it-CH", "Italian (Switzerland)" - }, - { - "it-IT", "Italian (Italy)" - }, - { - "ja-JP", "Japanese (Japan)" - }, - { - "kk-KZ", "Kazakh (Kazakhstan)" - }, - { - "ko-KR", "Korean (Korea)" - }, - { - "lt-LT", "Lithuanian (Lithuania)" - }, - { - "lv-LV", "Latvian (Latvia)" - }, - { - "mg-MG", "Malagasy (Madagascar)" - }, - { - "mk-MK", "Macedonian (Macedonia)" - }, - { - "ms-MY", "Malay (Malaysia)" - }, - { - "nb-NO", "Norwegian Bokmal (Norway)" - }, - { - "nl-BE", "Dutch (Belgium)" - }, - { - "nl-NL", "Dutch (Netherlands)" - }, - { - "nn-NO", "Norwegian Nynorsk (Norway)" - }, - { - "no-NO", "Norwegian (Norway)" - }, - { - "fa-IR", "Persian (Iran)" - }, - { - "pl-PL", "Polish (Poland)" - }, - { - "pt-BR", "Portuguese (Brazil)" - }, - { - "pt-PT", "Portuguese (Portugal)" - }, - { - "ro-RO", "Romanian (Romania)" - }, - { - "ru-RU", "Russian (Russia)" - }, - { - "ru-UA", "Russian (Ukraine)" - }, - { - "zh-CN", "Simplified Chinese (China)" - }, - { - "zh-HK", "Simplified Chinese (Hong Kong)" - }, - { - "zh-TW", "Simplified Chinese (Taiwan)" - }, - { - "sr-ME", "Serbian (Montenegro)" - }, - { - "sr-RS", "Serbian (Serbia)" - }, - { - "sk-SK", "Slovak (Slovakia)" - }, - { - "sl-SI", "Slovenian (Slovenia)" - }, - { - "es-AR", "Spanish (Argentina)" - }, - { - "es-BO", "Spanish (Bolivia)" - }, - { - "es-CL", "Spanish (Chile)" - }, - { - "es-CO", "Spanish (Colombia)" - }, - { - "es-CR", "Spanish (Costa Rica)" - }, - { - "es-DO", "Spanish (Dominican Republic)" - }, - { - "es-EC", "Spanish (Ecuador)" - }, - { - "es-SV", "Spanish (El Salvador)" - }, - { - "es-GT", "Spanish (Guatemala)" - }, - { - "es-HN", "Spanish (Honduras)" - }, - { - "es-LA", "Spanish (Latin America)" - }, - { - "es-MX", "Spanish (Mexico)" - }, - { - "es-NI", "Spanish (Nicaragua)" - }, - { - "es-PA", "Spanish (Panama)" - }, - { - "es-PE", "Spanish (Peru)" - }, - { - "es-PY", "Spanish (Paraguay)" - }, - { - "es-ES", "Spanish (Spain)" - }, - { - "es-US", "Spanish (United States)" - }, - { - "es-UY", "Spanish (Uruguay)" - }, - { - "sv-SE", "Swedish (Sweden)" - }, - { - "tl-PH", "Tagalog (Philippines)" - }, - { - "th-TH", "Thai (Thailand)" - }, - { - "ch-HK", "Traditional Chinese (Hong Kong)" - }, - { - "ch-TW", "Traditional Chinese (Taiwan)" - }, - { - "tr-TR", "Turkish (Turkey)" - }, - { - "uk-UA", "Ukrainian (Ukraine)" - }, - { - "vi-VN", "Vietnamese (Vietnam)" - }, - { - "af-ZA", "Afrikaans (South Africa)" - }, - { - "ar-EG", "Arabic (Egypt)" - }, - { - "as-IN", "Assamese (India)" - }, - { - "az-AZ", "Azerbaijani (Azerbaijan)" - }, - { - "bn-BD", "Bengali (Bangladesh)" - }, - { - "cy-GB", "Welsh (Wales)" - }, - { - "eu-ES", "Basque (Spain)" - }, - { - "ff-SN", "Fula (Senegal)" - }, - { - "gl-ES", "Galician (Spain)" - }, - { - "gu-IN", "Gujarati (India)" - }, - { - "ha-NG", "Hausa (Nigeria)" - }, - { - "ht-HT", "Haitian Creole (Haiti)" - }, - { - "hy-AM", "Armenian (Armenia)" - }, - { - "ig-NG", "Igbo (Nigeria)" - }, - { - "jv-ID", "Javanese (Indonesia)" - }, - { - "ka-GE", "Georgian (Georgia)" - }, - { - "km-KH", "Khmer (Cambodia)" - }, - { - "kn-IN", "Kannada (India)" - }, - { - "ku-TR", "Kurdish (Turkey)" - }, - { - "ky-KG", "Kyrgyz (Kyrgyzstan)" - }, - { - "mi-NZ", "Maori (New Zealand)" - }, - { - "ml-IN", "Malayalam (India)" - }, - { - "mn-MN", "Mongolian (Mongolia)" - }, - { - "mr-IN", "Marathi (India)" - }, - { - "my-MM", "Burmese (Myanmar)" - }, - { - "ne-NP", "Nepali (Nepal)" - }, - { - "om-ET", "Oromo (Ethiopia)" - }, - { - "pa-IN", "Punjabi (India)" - }, - { - "pa-PK", "Punjabi (Pakistan)" - }, - { - "sd-IN", "Sindhi (India)" - }, - { - "si-LK", "Sinhala (Sri Lanka)" - }, - { - "sn-ZW", "Shona (Zimbabwe)" - }, - { - "so-SO", "Somali (Somalia)" - }, - { - "sq-AL", "Albanian (Albania)" - }, - { - "su-ID", "Sundanese (Indonesia)" - }, - { - "sw-KE", "Swahili (Kenya)" - }, - { - "ta-IN", "Tamil (India)" - }, - { - "te-IN", "Telugu (India)" - }, - { - "ur-PK", "Urdu (Pakistan)" - }, - { - "uz-UZ", "Uzbek (Uzbekistan)" - }, - { - "wo-SN", "Wolof (Senegal)" - }, - { - "xh-ZA", "Xhosa (South Africa)" - }, - { - "yo-NG", "Yoruba (Nigeria)" - }, - { - "zu-ZA", "Zulu (South Africa)" - }, - { - "es-419", "Spanish (Latin America)" - }, - { - "zh-Hans-HK", "Simplified Chinese (Hong Kong)" - }, - { - "zh-Hant-HK", "Traditional Chinese (Hong Kong)" - }, - { - "zh-Hans-TW", "Simplified Chinese (Taiwan)" - }, - { - "zh-Hant-TW", "Traditional Chinese (Taiwan)" - }, - { - "ceb-PH", "Cebuano (Philippines)" - }, - { - "mai-IN", "Maithili (India)" - } - }; - - public static bool IsValidLanguageCode(string isoCode) - { - return AvailableLanguages.ContainsKey(isoCode); - } - - public static string? GetName(string isoCode) + private static readonly Dictionary AvailableLanguages = new() { - if (!AvailableLanguages.ContainsKey(isoCode)) { - return null; + "en-US", "English (United States)" + }, + { + "en-CA", "English (Canada)" + }, + { + "ar-BH", "Arabic (Bahrain)" + }, + { + "ar-KW", "Arabic (Kuwait)" + }, + { + "ar-LB", "Arabic (Lebanon)" + }, + { + "ar-OM", "Arabic (Oman)" + }, + { + "ar-QA", "Arabic (Qatar)" + }, + { + "ar-SA", "Arabic (Saudi Arabia)" + }, + { + "ar-AE", "Arabic (UAE)" + }, + { + "bs-BA", "Bosnian (Bosnia and Herzegovina)" + }, + { + "bg-BG", "Bulgarian (Bulgaria)" + }, + { + "ca-ES", "Catalan (Spain)" + }, + { + "hr-HR", "Croatian (Croatia)" + }, + { + "cs-CZ", "Czech (Czech Republic)" + }, + { + "da-DK", "Danish (Denmark)" + }, + { + "de-AT", "German (Austria)" + }, + { + "de-CH", "German (Switzerland)" + }, + { + "de-DE", "German (Germany)" + }, + { + "de-LU", "German (Luxembourg)" + }, + { + "el-GR", "Greek (Greece)" + }, + { + "en-AE", "English (UAE)" + }, + { + "en-AR", "English (Argentina)" + }, + { + "en-AU", "English (Australia)" + }, + { + "en-AT", "English (Austria)" + }, + { + "en-BG", "English (Bulgaria)" + }, + { + "en-BH", "English (Bahrain)" + }, + { + "en-BR", "English (Brazil)" + }, + { + "en-CL", "English (Chile)" + }, + { + "en-CO", "English (Colombia)" + }, + { + "en-HR", "English (Croatia)" + }, + { + "en-CY", "English (Cyprus)" + }, + { + "en-CZ", "English (Czech Republic)" + }, + { + "en-DK", "English (Denmark)" + }, + { + "en-FI", "English (Finland)" + }, + { + "en-DE", "English (Germany)" + }, + { + "en-GB", "English (Great Britain)" + }, + { + "en-GR", "English (Greece)" + }, + { + "en-HK", "English (Hong Kong)" + }, + { + "en-HU", "English (Hungary)" + }, + { + "en-IS", "English (Iceland)" + }, + { + "en-IN", "English (India)" + }, + { + "en-ID", "English (Indonesia)" + }, + { + "en-IE", "English (Ireland)" + }, + { + "en-IL", "English (Israel)" + }, + { + "en-KW", "English (Kuwait)" + }, + { + "en-LV", "English (Latvia)" + }, + { + "en-LB", "English (Lebanon)" + }, + { + "en-MY", "English (Malaysia)" + }, + { + "en-MT", "English (Malta)" + }, + { + "en-MX", "English (Mexico)" + }, + { + "en-NL", "English (Netherlands)" + }, + { + "en-NZ", "English (New Zealand)" + }, + { + "en-NO", "English (Norway)" + }, + { + "en-OM", "English (Oman)" + }, + { + "en-PE", "English (Peru)" + }, + { + "en-PL", "English (Poland)" + }, + { + "en-QA", "English (Qatar)" + }, + { + "en-RO", "English (Romania)" + }, + { + "en-SA", "English (Saudi Arabia)" + }, + { + "en-SE", "English (Sweden)" + }, + { + "en-SG", "English (Singapore)" + }, + { + "en-SK", "English (Slovakia)" + }, + { + "en-SI", "English (Slovenia)" + }, + { + "en-TW", "English (Taiwan)" + }, + { + "en-TH", "English (Thailand)" + }, + { + "en-TR", "English (Turkey)" + }, + { + "en-ZA", "English (South Africa)" + }, + { + "et-EE", "Estonian (Estonia)" + }, + { + "fi-FI", "Finnish (Finland)" + }, + { + "fr-BE", "French (Belgium)" + }, + { + "fr-CA", "French (Canada)" + }, + { + "fr-FR", "French (France)" + }, + { + "fr-LU", "French (Luxembourg)" + }, + { + "fr-CH", "French (Switzerland)" + }, + { + "he-IL", "Hebrew (Israel)" + }, + { + "hi-IN", "Hindi (India)" + }, + { + "hu-HU", "Hungarian (Hungary)" + }, + { + "is-IS", "Icelandic (Iceland)" + }, + { + "id-ID", "Indonesian (Indonesia)" + }, + { + "it-CH", "Italian (Switzerland)" + }, + { + "it-IT", "Italian (Italy)" + }, + { + "ja-JP", "Japanese (Japan)" + }, + { + "kk-KZ", "Kazakh (Kazakhstan)" + }, + { + "ko-KR", "Korean (Korea)" + }, + { + "lt-LT", "Lithuanian (Lithuania)" + }, + { + "lv-LV", "Latvian (Latvia)" + }, + { + "mg-MG", "Malagasy (Madagascar)" + }, + { + "mk-MK", "Macedonian (Macedonia)" + }, + { + "ms-MY", "Malay (Malaysia)" + }, + { + "nb-NO", "Norwegian Bokmal (Norway)" + }, + { + "nl-BE", "Dutch (Belgium)" + }, + { + "nl-NL", "Dutch (Netherlands)" + }, + { + "nn-NO", "Norwegian Nynorsk (Norway)" + }, + { + "no-NO", "Norwegian (Norway)" + }, + { + "fa-IR", "Persian (Iran)" + }, + { + "pl-PL", "Polish (Poland)" + }, + { + "pt-BR", "Portuguese (Brazil)" + }, + { + "pt-PT", "Portuguese (Portugal)" + }, + { + "ro-RO", "Romanian (Romania)" + }, + { + "ru-RU", "Russian (Russia)" + }, + { + "ru-UA", "Russian (Ukraine)" + }, + { + "zh-CN", "Simplified Chinese (China)" + }, + { + "zh-HK", "Simplified Chinese (Hong Kong)" + }, + { + "zh-TW", "Simplified Chinese (Taiwan)" + }, + { + "sr-ME", "Serbian (Montenegro)" + }, + { + "sr-RS", "Serbian (Serbia)" + }, + { + "sk-SK", "Slovak (Slovakia)" + }, + { + "sl-SI", "Slovenian (Slovenia)" + }, + { + "es-AR", "Spanish (Argentina)" + }, + { + "es-BO", "Spanish (Bolivia)" + }, + { + "es-CL", "Spanish (Chile)" + }, + { + "es-CO", "Spanish (Colombia)" + }, + { + "es-CR", "Spanish (Costa Rica)" + }, + { + "es-DO", "Spanish (Dominican Republic)" + }, + { + "es-EC", "Spanish (Ecuador)" + }, + { + "es-SV", "Spanish (El Salvador)" + }, + { + "es-GT", "Spanish (Guatemala)" + }, + { + "es-HN", "Spanish (Honduras)" + }, + { + "es-LA", "Spanish (Latin America)" + }, + { + "es-MX", "Spanish (Mexico)" + }, + { + "es-NI", "Spanish (Nicaragua)" + }, + { + "es-PA", "Spanish (Panama)" + }, + { + "es-PE", "Spanish (Peru)" + }, + { + "es-PY", "Spanish (Paraguay)" + }, + { + "es-ES", "Spanish (Spain)" + }, + { + "es-US", "Spanish (United States)" + }, + { + "es-UY", "Spanish (Uruguay)" + }, + { + "sv-SE", "Swedish (Sweden)" + }, + { + "tl-PH", "Tagalog (Philippines)" + }, + { + "th-TH", "Thai (Thailand)" + }, + { + "ch-HK", "Traditional Chinese (Hong Kong)" + }, + { + "ch-TW", "Traditional Chinese (Taiwan)" + }, + { + "tr-TR", "Turkish (Turkey)" + }, + { + "uk-UA", "Ukrainian (Ukraine)" + }, + { + "vi-VN", "Vietnamese (Vietnam)" + }, + { + "af-ZA", "Afrikaans (South Africa)" + }, + { + "ar-EG", "Arabic (Egypt)" + }, + { + "as-IN", "Assamese (India)" + }, + { + "az-AZ", "Azerbaijani (Azerbaijan)" + }, + { + "bn-BD", "Bengali (Bangladesh)" + }, + { + "cy-GB", "Welsh (Wales)" + }, + { + "eu-ES", "Basque (Spain)" + }, + { + "ff-SN", "Fula (Senegal)" + }, + { + "gl-ES", "Galician (Spain)" + }, + { + "gu-IN", "Gujarati (India)" + }, + { + "ha-NG", "Hausa (Nigeria)" + }, + { + "ht-HT", "Haitian Creole (Haiti)" + }, + { + "hy-AM", "Armenian (Armenia)" + }, + { + "ig-NG", "Igbo (Nigeria)" + }, + { + "jv-ID", "Javanese (Indonesia)" + }, + { + "ka-GE", "Georgian (Georgia)" + }, + { + "km-KH", "Khmer (Cambodia)" + }, + { + "kn-IN", "Kannada (India)" + }, + { + "ku-TR", "Kurdish (Turkey)" + }, + { + "ky-KG", "Kyrgyz (Kyrgyzstan)" + }, + { + "mi-NZ", "Maori (New Zealand)" + }, + { + "ml-IN", "Malayalam (India)" + }, + { + "mn-MN", "Mongolian (Mongolia)" + }, + { + "mr-IN", "Marathi (India)" + }, + { + "my-MM", "Burmese (Myanmar)" + }, + { + "ne-NP", "Nepali (Nepal)" + }, + { + "om-ET", "Oromo (Ethiopia)" + }, + { + "pa-IN", "Punjabi (India)" + }, + { + "pa-PK", "Punjabi (Pakistan)" + }, + { + "sd-IN", "Sindhi (India)" + }, + { + "si-LK", "Sinhala (Sri Lanka)" + }, + { + "sn-ZW", "Shona (Zimbabwe)" + }, + { + "so-SO", "Somali (Somalia)" + }, + { + "sq-AL", "Albanian (Albania)" + }, + { + "su-ID", "Sundanese (Indonesia)" + }, + { + "sw-KE", "Swahili (Kenya)" + }, + { + "ta-IN", "Tamil (India)" + }, + { + "te-IN", "Telugu (India)" + }, + { + "ur-PK", "Urdu (Pakistan)" + }, + { + "uz-UZ", "Uzbek (Uzbekistan)" + }, + { + "wo-SN", "Wolof (Senegal)" + }, + { + "xh-ZA", "Xhosa (South Africa)" + }, + { + "yo-NG", "Yoruba (Nigeria)" + }, + { + "zu-ZA", "Zulu (South Africa)" + }, + { + "es-419", "Spanish (Latin America)" + }, + { + "zh-Hans-HK", "Simplified Chinese (Hong Kong)" + }, + { + "zh-Hant-HK", "Traditional Chinese (Hong Kong)" + }, + { + "zh-Hans-TW", "Simplified Chinese (Taiwan)" + }, + { + "zh-Hant-TW", "Traditional Chinese (Taiwan)" + }, + { + "ceb-PH", "Cebuano (Philippines)" + }, + { + "mai-IN", "Maithili (India)" } + }; - AvailableLanguages.TryGetValue(isoCode, out var name); - return name; - } + public static bool IsValidLanguageCode(string isoCode) => AvailableLanguages.ContainsKey(isoCode); + + public static string? GetName(string isoCode) => AvailableLanguages.TryGetValue(isoCode, out var name) ? name : null; public static string? GetCode(string codeName) { - if (!AvailableLanguages.ContainsValue(codeName)) + foreach (var pair in AvailableLanguages) { - return null; + if (string.Equals(pair.Value, codeName, StringComparison.Ordinal)) + return pair.Key; } - var pair = AvailableLanguages.FirstOrDefault(x => x.Value == codeName); - return pair.Key; + return null; } } \ No newline at end of file diff --git a/src/SharedKernel/SharedKernel.csproj b/src/SharedKernel/SharedKernel.csproj index f2facdc..aab8967 100644 --- a/src/SharedKernel/SharedKernel.csproj +++ b/src/SharedKernel/SharedKernel.csproj @@ -2,77 +2,86 @@ net10.0 - enable enable - pandatech.png - Readme.md - Pandatech - MIT - 2.1.4 + enable + + true + Pandatech.SharedKernel - Pandatech Shared Kernel Library - Pandatech, shared kernel, library, OpenAPI, Swagger, utilities, scalar - Pandatech.SharedKernel provides centralized configurations, utilities, and extensions for ASP.NET Core projects. For more information refere to readme.md document. + Pandatech + Opinionated ASP.NET Core 10 infrastructure kernel: OpenAPI (Swagger + Scalar), Serilog, MediatR, FluentValidation, CORS, SignalR, OpenTelemetry, health checks, maintenance mode, resilience pipelines, and shared utilities. + aspnetcore;serilog;mediatr;fluentvalidation;openapi;swagger;scalar;opentelemetry;signalr;healthchecks;resilience;shared-kernel;pandatech + + MIT + git https://github.com/PandaTechAM/be-lib-sharedkernel - Updated ResponseCrafter library + https://github.com/PandaTechAM/be-lib-sharedkernel + true + pandatech.png + README.md - - false + 2.2.0 + Refactored documentations, updated nugets - - true + true + true + snupkg + true - + false + true false - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/SharedKernel.Tests/SharedKernel.Tests.csproj b/test/SharedKernel.Tests/SharedKernel.Tests.csproj index c469aaf..1e0af14 100644 --- a/test/SharedKernel.Tests/SharedKernel.Tests.csproj +++ b/test/SharedKernel.Tests/SharedKernel.Tests.csproj @@ -10,13 +10,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all