↑↓ to navigate Enter to open "…" all these words ANDOR to combine

Onboarding guide

28. Common Cloud Provider Adapters (AWS, Azure, GCP)

This chapter covers the three vendor adapter packages, MMCA.Common.Aws, MMCA.Common.Azure and MMCA.Common.Gcp. The rule they exist to keep is short: the core packages reference no cloud SDK. Every vendor integration lives in an adapter package that references the core, never the reverse, and a host opts in with one registration call per capability (ADR-134). So the core defines an abstraction (or an extension point) and ships an inert default. Each adapter type implements that abstraction against one vendor SDK, and all a host has to decide is which package to reference and which calls to make. [Rubric §3, Clean Architecture] asks whether dependencies point inward toward policy and away from infrastructure detail. Here the vendor SDKs sit at the outermost ring, and nothing in Domain, Application or Infrastructure names them. [Rubric §32, Dependency & Supply-Chain] asks whether a consumer pays only for the dependencies it uses. A host that never calls an AWS adapter never restores an AWS package.

The abstractions the adapters implement. Five extension points in the core take an adapter. Their contracts are taught where they are declared, and this chapter only implements them:

  • File storage: IFileStorageService (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/Storage/IFileStorageService.cs:15, ADR-045). The core default is NullFileStorageService (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Storage/NullFileStorageService.cs:14).
  • Native push: the pair INativePushSender (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/Notifications/INativePushSender.cs:12) and IPushDeviceRegistrar (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/Notifications/IPushDeviceRegistrar.cs:11) (ADR-044). The core defaults are NullNativePushSender and NullPushDeviceRegistrar (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Notifications/Push/NullNativePushSender.cs:12, NullPushDeviceRegistrar.cs:14).
  • Broker transport: IBrokerTransport (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Messaging/Transports/IBrokerTransport.cs:25), registered through AddBrokerTransport (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.Messaging.cs:159) and selected by name (ADR-066, ADR-118). The retry, redelivery and backpressure rules every transport shares live once in BrokerTransportPolicies.
  • PostgreSQL connection customization: INpgsqlDataSourceConfigurator (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/DbContexts/INpgsqlDataSourceConfigurator.cs:23), added through AddNpgsqlDataSourceConfigurator (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.Persistence.cs:24).
  • Host-level ASP.NET Core extension points: the configuration system (IConfigurationSource), the DataProtection key ring (IXmlRepository, IXmlEncryptor, IXmlDecryptor), and the OpenTelemetry pipeline that AddServiceDefaults() configures. These are framework types, not MMCA ones, so the adapters plug into them directly.

Per-vendor membership. The 67 types fall into one row per capability. A blank cell means the vendor package has no adapter for that capability.

Capability (core hook) MMCA.Common.Aws (22) MMCA.Common.Azure (12) MMCA.Common.Gcp (33)
Registration entry point AwsServiceCollectionExtensions AzureServiceCollectionExtensions GcpServiceCollectionExtensions
Broker transport (IBrokerTransport) AmazonSqsTransport, AmazonSqsTransportSettings AzureServiceBusTransport, ServiceBusEmulatorSupport (none)
Native push (INativePushSender + IPushDeviceRegistrar) SnsNativePushSender, SnsPushDeviceRegistrar, SnsInstallationRegistry, SnsInstallation, SnsNativePushSettings, SnsPushMessage, SnsPushClientFactory AzureNotificationHubNativePushSender, AzureNotificationHubDeviceRegistrar, AzureNotificationHubSettings, NativePushUserTags FcmNativePushSender, FcmPushDeviceRegistrar, IFcmInstallationRegistry, FirestoreInstallationRegistry, FcmInstallation, IFcmMessagingClient, FirebaseMessagingClient, FcmPushClientFactory, FcmPushMessage, FcmSendOutcome, FcmNativePushSettings
File storage (IFileStorageService) S3FileStorageService, S3FileStorageSettings, S3StorageClientFactory, S3ObjectUrl AzureBlobFileStorageService, AzureBlobFileStorageSettings GcsFileStorageService, GcsFileStorageSettings, GcsStorageClientFactory, GcsObjectUrl
Secrets as configuration (IConfigurationSource) AwsSecretsManagerConfigurationExtensions, AwsSecretsManagerConfigurationSource, AwsSecretsManagerConfigurationProvider AzureKeyVaultConfigurationExtensions GoogleSecretManagerConfigurationExtensions, GoogleSecretManagerConfigurationSource, GoogleSecretManagerConfigurationProvider
PostgreSQL IAM auth (INpgsqlDataSourceConfigurator) RdsIamAuthenticationConfigurator, RdsIamAuthenticationSettings (none) CloudSqlIamAuthenticationConfigurator, CloudSqlIamAuthenticationSettings, CloudSqlAccessTokenSource
Shared DataProtection key ring AwsDataProtectionExtensions AzureDataProtectionExtensions GoogleCloudDataProtectionExtensions, GcpDataProtectionBuilderExtensions, GcsXmlRepository, GcpKmsXmlEncryptor, GcpKmsXmlDecryptor, GcsObjectLocation
Vendor telemetry exporter (none) AzureMonitorExporterExtensions GoogleCloudTelemetryExporterExtensions, GoogleCloudTelemetryAuthHandler, GoogleCloudTelemetryAccessToken, IGoogleCloudTelemetryTokenSource, ApplicationDefaultTelemetryTokenSource
Shared AWS client plumbing AwsClientEndpoint, AwsStaticCredentials

The rows are uneven for real reasons, not because a port is unfinished. GCP ships no broker transport. Its Cloud SQL configurator instead covers the built-in PostgreSQL broker alongside the database contexts (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:71-73). Azure ships no Npgsql configurator. Its database story is Entra ID for SQL Server, which arrives through a package reference with no registration call (ADR-134, Decision). AWS ships no telemetry exporter call. The GCP notifications namespace is the largest because FCM tokens carry no owner, so the package adds its own registry and client interfaces (IFcmInstallationRegistry, IFcmMessagingClient) to put Firestore and FirebaseAdmin behind something testable (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FirestoreInstallationRegistry.cs:16, FirebaseMessagingClient.cs:17).

How the swap works at runtime: inert core defaults, then an adapter override. AddInfrastructure registers every core default with TryAdd: services.TryAddTransient<INativePushSender, NullNativePushSender>() and its registrar sibling (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:353-354), and services.TryAddTransient<IFileStorageService, NullFileStorageService>() (DependencyInjection.cs:361). The adapters register with a plain Add: services.AddTransient<IFileStorageService, AzureBlobFileStorageService>() (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:68), S3FileStorageService (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:67), GcsFileStorageService (GcpServiceCollectionExtensions.cs:62), and the push pairs (AzureServiceCollectionExtensions.cs:101-102, GcpServiceCollectionExtensions.cs:162-163). The adapter therefore wins whichever side runs first. Called before AddInfrastructure, it makes the core TryAdd skip. Called after, it is the last registration, which is the one a single-service resolve returns. The vendor SDK clients themselves go in with TryAddSingleton (for example the BlobContainerClient at AzureServiceCollectionExtensions.cs:61-67, S3StorageClientFactory.Create at AwsServiceCollectionExtensions.cs:66, and the Firestore and FCM clients at GcpServiceCollectionExtensions.cs:159-161). A host that registered its own client first, an emulator endpoint for example, keeps it. [Rubric §2, Design Patterns] asks whether patterns solve a real problem. This is the Adapter pattern with a Null Object default: application code depends on one interface, and swapping the cloud is a registration change.

How a host selects a vendor: reference, call, then configuration as the gate. Every adapter call is unconditional in the host and a no-op until its configuration is complete. The class summaries say so in identical words (AzureServiceCollectionExtensions.cs:17-23, AwsServiceCollectionExtensions.cs:19-24, GcpServiceCollectionExtensions.cs:16-21), and the bodies enforce it with early returns. Azure Blob needs a ContainerName plus an absolute ServiceUri or a ConnectionString (AzureServiceCollectionExtensions.cs:49-59). S3 needs a Bucket (AwsServiceCollectionExtensions.cs:58-61). GCS needs a GcsBucket (GcpServiceCollectionExtensions.cs:56-59). Push needs NativePush:Enabled plus the vendor's sub-section (AzureServiceCollectionExtensions.cs:91-96, GcpServiceCollectionExtensions.cs:148-153). The three storage adapters read the same FileStorage section, each through its own gate key, so a section that carries only the S3 Bucket or the Azure ContainerName leaves the GCS call inert (GcpServiceCollectionExtensions.cs:32-40). The builder-level calls follow the same posture with a single gate key each: KeyVault:Uri (AzureKeyVaultConfigurationExtensions.cs:80), SecretsManager:NamePrefix (AwsSecretsManagerConfigurationExtensions.cs:82), DataProtection:BlobStorageUri, SsmParameterPrefix or GcsObjectUri (AzureDataProtectionExtensions.cs:53, AwsDataProtectionExtensions.cs:57, GoogleCloudDataProtectionExtensions.cs:76), APPLICATIONINSIGHTS_CONNECTION_STRING (AzureMonitorExporterExtensions.cs:42) and GoogleCloudTelemetry:ProjectId (GoogleCloudTelemetryExporterExtensions.cs:101). The result is that local development and tests take no cloud dependency at startup, and a deployment switches a capability on through configuration alone. The secrets sources are deliberately not called from AddServiceDefaults() for exactly that reason. They also use one naming convention (a double dash maps to the configuration separator, so ConnectionStrings--Default arrives as ConnectionStrings:Default), which lets a secret set move between Key Vault and Secrets Manager by renaming its prefix alone. Both points are recorded on AzureKeyVaultConfigurationExtensions.cs:80 and AwsSecretsManagerConfigurationExtensions.cs:82.

Broker transports are selected by name, not by type. AddBrokerMessaging picks the bus from the string MessageBus:Provider. RabbitMq, PostgreSql and InProcess are built in, and any other name must match an IBrokerTransport an adapter registered earlier. A name with no registered transport throws, naming the package and the call (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.Messaging.cs:24-36). The two adapter calls are therefore one-liners that hand a transport instance to AddBrokerTransport: AddAzureServiceBusTransport() (AzureServiceCollectionExtensions.cs:117-118) and AddAmazonSqsTransport() (AwsServiceCollectionExtensions.cs:85-86). Each transport reports its provider name through Name (MMCA.Common/Source/Hosting/MMCA.Common.Azure/Messaging/AzureServiceBusTransport.cs:22, MMCA.Common/Source/Hosting/MMCA.Common.Aws/Messaging/AmazonSqsTransport.cs:69). The ordering is load-bearing: the adapter call must come BEFORE AddBrokerMessaging, which picks the transport while it registers the bus (AzureServiceCollectionExtensions.cs:108-110, AwsServiceCollectionExtensions.cs:73-75). Vendor limits surface at registration. The SQS transport defaults an unconfigured MessageBus:RedeliveryIntervalsSeconds to [60, 300, 900], and a configured value over 900 (the SQS delay cap) makes AddBrokerMessaging throw (AwsServiceCollectionExtensions.cs:78-81). A Service Bus connection string carrying UseDevelopmentEmulator=true targets the local emulator through ServiceBusEmulatorSupport and needs MessageBus:EmulatorAdminEndpoint (AzureServiceCollectionExtensions.cs:111-113). [Rubric §10, Messaging & Integration] asks whether transport choice is isolated from message semantics. Here the semantics live in the core and its shared policies, and the adapter supplies only the wire.

Identity instead of stored credentials. Each adapter defaults to the platform's ambient identity and treats a static key as the exception. Azure uses DefaultAzureCredential for Blob, Key Vault and the DataProtection blob (AzureServiceCollectionExtensions.cs:64, AzureKeyVaultConfigurationExtensions.cs:80, AzureDataProtectionExtensions.cs:53). GCP uses Application Default Credentials and has no key-file or HMAC setting at all (GcpServiceCollectionExtensions.cs:37-38, :123-125). AWS falls back to the default credential chain unless AccessKeyId and SecretAccessKey are both set. AwsStaticCredentials enforces the both-or-neither rule at registration rather than on the first call (AwsServiceCollectionExtensions.cs:63-64, :117-120). The two PostgreSQL configurators push the same idea down to the database connection. RdsIamAuthenticationConfigurator and CloudSqlIamAuthenticationConfigurator both implement INpgsqlDataSourceConfigurator (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Rds/RdsIamAuthenticationConfigurator.cs:20-21, MMCA.Common/Source/Hosting/MMCA.Common.Gcp/CloudSql/CloudSqlIamAuthenticationConfigurator.cs:23). Each replaces the password with a short-lived token: an RDS token refreshed every 10 minutes, or a Cloud SQL OAuth2 token refreshed every minute, retried 30 seconds after a failure in both cases (AwsServiceCollectionExtensions.cs:89-92, GcpServiceCollectionExtensions.cs:68-72). Because that token travels as the password, both registration calls refuse any configured PostgreSQL connection string that does not require TLS, and they refuse it at startup, not on the first query (AwsServiceCollectionExtensions.cs:122-124, GcpServiceCollectionExtensions.cs:100-102). [Rubric §11, Security] asks whether secrets are kept out of code and configuration and whether credentials are short-lived. These adapters are where the framework answers that question for each cloud.

Scale-out and observability concerns at the host level. The three DataProtection calls fix one specific multi-replica failure. The framework default keeps the key ring in memory, so a cookie or antiforgery token minted by one replica cannot be decrypted by another. Persisting the ring to one shared store (an Azure blob, SSM Parameter Store, or one Cloud Storage object) gives every replica the same keys (the class summaries above AzureDataProtectionExtensions.cs:20, AwsDataProtectionExtensions.cs:21 and GoogleCloudDataProtectionExtensions.cs:22). GCP is the one vendor that builds its own repository and KMS encryptor and decryptor: GcsXmlRepository implements IXmlRepository, and GcpKmsXmlEncryptor and GcpKmsXmlDecryptor implement the framework's XML encryption interfaces (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcsXmlRepository.cs:31, GcpKmsXmlEncryptor.cs:25, GcpKmsXmlDecryptor.cs:16). It rewrites the single object under a generation precondition so two replicas cannot overwrite each other's new key (documented on GoogleCloudDataProtectionExtensions.cs:76). The two telemetry calls add a vendor exporter beside the vendor-neutral OTLP exporter that AddServiceDefaults() configures, and both must run AFTER it so the service-defaults filter processors see spans first. Both enforce that order even when their gate key is unset, so a misordered host fails on a developer machine (AzureMonitorExporterExtensions.cs:42, GoogleCloudTelemetryExporterExtensions.cs:101). The Google exporter sends traces and logs only. Metrics go through a collector because the Telemetry API caps a metric request at 200 data points (recorded on GoogleCloudTelemetryExporterExtensions.cs:101). Its token is fetched by the hosted service GoogleCloudTelemetryAccessToken at host start, never at registration (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Telemetry/GoogleCloudTelemetryAccessToken.cs:28-30). [Rubric §13, Observability & Operability] asks whether telemetry reaches an operator without coupling code to a backend. Here the instrumentation stays vendor-neutral and only the exporter is chosen per cloud.

Guardrails against a silently missing adapter. A quiet default is right for a host that never switched a capability on and wrong for one that did. The core closes that gap with the internal CloudAdapterTripwire options validator, wired with ValidateOnStart in AddInfrastructure (DependencyInjection.cs:217-223, class at MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Configuration/CloudAdapterTripwire.cs:30). When NativePush:Enabled is true but the resolved INativePushSender is still NullNativePushSender, the host fails startup with a message naming the adapter call to add (CloudAdapterTripwire.cs:48-53). File storage is deliberately unchecked. A FileStorage section is routinely shared across every service of a solution, so it does not declare intent, and a host that does store files without an adapter is not silent anyway, because NullFileStorageService fails every upload with FileStorage.NotConfigured (CloudAdapterTripwire.cs:22-27). The dependency direction itself is a fitness test. CloudVendorIsolationTestsBase asserts that the core references no vendor SDK, beside a compile-time EnforceCloudVendorBoundary guard (ADR-134, Decision; restated in MMCA.Common/AGENTS.md, "Cloud vendor adapters"). [Rubric §34, Architecture Governance & Documentation] asks whether a decision is enforced rather than only written down, and this one is enforced twice.

Where it is used. MMCA.ADC is the Azure adopter, and every call it makes comes from MMCA.Common.Azure. Each service host calls AddAzureKeyVaultConfiguration() and AddAzureMonitorExporter() (for example MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:115,122). The broker services call AddAzureServiceBusTransport() (MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:428, MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:339). Conference and Identity call AddAzureBlobFileStorage (MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:367, MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:261). The Notification module calls AddAzureNotificationHubsPush (MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.API/DependencyInjection.cs:40). Identity and the Blazor web host call AddAzureDataProtection() (MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:127, MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:72). No host under MMCA.ADC/Source calls an AWS or GCP adapter. Those two packages are exercised by Common's own LocalStack and emulator test tiers, not by an MMCA.ADC deployment. The registration calls are C# extension(T) members, read as described in the primer.

AmazonSqsTransportSettings

MMCA.Common.Aws · MMCA.Common.Aws.Messaging · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Messaging/AmazonSqsTransportSettings.cs:8 · Level 0 · class

  • What it is: the bound options class for the Amazon SQS/SNS broker transport, read from the MessageBus:AmazonSqs section (AmazonSqsTransportSettings.cs:11).
  • Depends on: nothing first-party; System.Uri for the endpoint override.
  • Concept: a vendor-specific sub-section of the provider-neutral MessageBusSettings. The neutral settings carry the redelivery policy; this class carries only what SQS addressing needs. [Rubric §10, Messaging & Integration] assesses whether broker choice is a configuration concern rather than a code change; here the SQS addressing lives in its own section so the shared MessageBus keys stay transport-agnostic.
  • Walkthrough: four init properties. Region (AmazonSqsTransportSettings.cs:18) is a region system name; blank defers to the AWS SDK fallback chain, and no region at all fails registration (enforced by AmazonSqsTransport). ServiceUrl (AmazonSqsTransportSettings.cs:24) is one endpoint override for both SQS and SNS, honored only when absolute. AccessKeyId / SecretAccessKey (AmazonSqsTransportSettings.cs:31,34) are an optional static key pair, both-or-neither (enforced by AwsStaticCredentials).
  • Why it's built this way: every field is optional because the AWS default chains (region and credentials) are the production path for an IAM-role host; the static pair and endpoint override exist for LocalStack and non-AWS environments. Transport selection by configuration is recorded in ADR-066, the fail-at-registration posture in ADR-070.
  • Where it's used: read once inside AmazonSqsTransport.Configure (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Messaging/AmazonSqsTransport.cs:78-79) and named in its error messages (AmazonSqsTransport.cs:87,183).

AwsClientEndpoint

MMCA.Common.Aws · MMCA.Common.Aws · MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsClientEndpoint.cs:11 · Level 0 · class (internal static)

  • What it is: a one-method helper that points any AWS SDK ClientConfig at either a custom endpoint or an AWS region, from the ServiceUrl / Region pair every AWS settings section in this package carries.
  • Depends on: nothing first-party; AWS SDK ClientConfig and RegionEndpoint.
  • Concept: one shared rule for endpoint resolution, so SNS, DynamoDB, S3 and Secrets Manager clients interpret the same two keys identically. [Rubric §15, Best Practices & Code Quality] assesses whether duplicated logic is factored once; this helper is that single copy.
  • Walkthrough: Apply(config, serviceUrl, region) (AwsClientEndpoint.cs:23). An absolute serviceUrl wins: it sets config.ServiceURL (AwsClientEndpoint.cs:29) and, when a region is also given, uses it as the signing region via AuthenticationRegion (AwsClientEndpoint.cs:32). Otherwise a non-blank region sets RegionEndpoint (AwsClientEndpoint.cs:37). Neither set leaves the client on the SDK region fallback chain.
  • Why it's built this way: the doc comment records that the SDK treats the two as mutually exclusive (setting a region endpoint clears the service URL, AwsClientEndpoint.cs:16-17), hence the explicit branch rather than setting both. The endpoint override is what lets LocalStack, MinIO and Cloudflare R2 stand in for AWS (AwsClientEndpoint.cs:13-15).
  • Where it's used: SnsPushClientFactory (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsPushClientFactory.cs:23,39), S3StorageClientFactory, and AwsSecretsManagerConfigurationExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsSecretsManagerConfigurationExtensions.cs:118). AmazonSqsTransport does not use it: MassTransit's host configurator takes the region and endpoint separately (AmazonSqsTransport.cs:92,102-103).

AwsStaticCredentials

MMCA.Common.Aws · MMCA.Common.Aws · MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsStaticCredentials.cs:9 · Level 0 · class (internal static)

  • What it is: the shared both-or-neither rule for an AccessKeyId / SecretAccessKey pair.
  • Depends on: nothing first-party; AWS SDK BasicAWSCredentials.
  • Concept: a half-configured credential is a configuration error, not a fallback. [Rubric §11, Security] assesses whether identity is explicit and least-surprise; the doc comment states the reason directly: silently falling back to the default chain "would run the host under an identity nobody configured" (AwsStaticCredentials.cs:14-15).
  • Walkthrough: Resolve(accessKeyId, secretAccessKey, sectionPath) (AwsStaticCredentials.cs:22) returns a BasicAWSCredentials when both halves are set (AwsStaticCredentials.cs:27-29), throws InvalidOperationException naming both keys under sectionPath when exactly one is set (AwsStaticCredentials.cs:32-35), and returns null when both are blank (AwsStaticCredentials.cs:38), which callers read as "use the AWS default credential chain".
  • Why it's built this way: the check runs at registration time in every caller, matching ADR-070: a misconfiguration surfaces at startup rather than on the first upload, send or query.
  • Where it's used: AwsServiceCollectionExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:64,117,178), AwsSecretsManagerConfigurationExtensions (AwsSecretsManagerConfigurationExtensions.cs:97), AmazonSqsTransport (AmazonSqsTransport.cs:86) and S3StorageClientFactory.

RdsIamAuthenticationSettings

MMCA.Common.Aws · MMCA.Common.Aws.Rds · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Rds/RdsIamAuthenticationSettings.cs:8 · Level 0 · class (internal sealed)

  • What it is: the bound options for RDS IAM database authentication, section Rds:IamAuthentication (RdsIamAuthenticationSettings.cs:11).
  • Depends on: nothing first-party.
  • Concept: cross-reference RdsIamAuthenticationConfigurator, which introduces token-as-password authentication.
  • Walkthrough: Enabled (RdsIamAuthenticationSettings.cs:14) is the gate, off when absent. Region (RdsIamAuthenticationSettings.cs:21) is the database's region, blank deferring to the SDK chain. AccessKeyId / SecretAccessKey (RdsIamAuthenticationSettings.cs:28,31) follow the AwsStaticCredentials both-or-neither rule.
  • Why it's built this way: an explicit boolean gate (rather than presence of a key) lets a deployment switch PostgreSQL onto IAM tokens by configuration alone; see ADR-113 and ADR-134.
  • Where it's used: bound in AddAwsRdsIamAuthentication (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:111); SectionPath is quoted in the TLS error of RdsIamAuthenticationConfigurator (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Rds/RdsIamAuthenticationConfigurator.cs:116).

SnsInstallation

MMCA.Common.Aws · MMCA.Common.Aws.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsInstallation.cs:14 · Level 0 · record (internal sealed)

  • What it is: the in-memory shape of one row of the DynamoDB installation registry: InstallationId, UserId, Platform, PushChannel, EndpointArn, and an optional SubscriptionArn (SnsInstallation.cs:14-20).
  • Depends on: nothing first-party.
  • Concept: cross-reference SnsInstallationRegistry, which explains why SNS needs a side table at all (SNS keeps no owner per endpoint).
  • Walkthrough: a positional record; SubscriptionArn is the only nullable member because a row can exist before or without a topic subscription. UserId holds the invariant string form produced by SnsInstallationRegistry.UserKey (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsInstallationRegistry.cs:50-51), not the typed identifier.
  • Where it's used: materialized by SnsInstallationRegistry.FromItem (SnsInstallationRegistry.cs:227-245), written by TryPutAsync (SnsInstallationRegistry.cs:75), built by SnsPushDeviceRegistrar (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsPushDeviceRegistrar.cs:170), and grouped by endpoint in SnsNativePushSender (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsNativePushSender.cs:58).

AwsDataProtectionExtensions

MMCA.Common.Aws · MMCA.Common.Aws · MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsDataProtectionExtensions.cs:21 · Level 1 · class (static, extension<TBuilder>)

  • What it is: AddAwsDataProtection(), which persists the ASP.NET Core DataProtection key ring to AWS Systems Manager Parameter Store so every replica of a scaled-out host shares one key ring.
  • Depends on: HostIntegrationMarker and HostIntegrationNames; externals IHostApplicationBuilder, AddDataProtection, and the AWS PersistKeysToAWSSystemsManager extension.
  • Concept: a configuration-gated host integration with a startup receipt. The method is a no-op unless its gate key is set; when it does run, it registers a marker the core startup tripwire looks for, so a host that has the key but forgot the call fails instead of running every replica on its own key ring (AwsDataProtectionExtensions.cs:70-72). It is declared as a C# extension<TBuilder> block (AwsDataProtectionExtensions.cs:23-24; see C# extension(T) types). [Rubric §29, Resilience & Business Continuity] assesses whether scale-out does not break user sessions; a shared key ring is what keeps antiforgery tokens and auth cookies valid across replicas.
  • Walkthrough: reads DataProtection:SsmParameterPrefix (AwsDataProtectionExtensions.cs:59) and returns unchanged when blank (AwsDataProtectionExtensions.cs:65-68), deliberately registering no receipt so a prefix added by a later source still trips the check (AwsDataProtectionExtensions.cs:61-64). Otherwise it registers HostIntegrationMarker(HostIntegrationNames.AwsDataProtection) (AwsDataProtectionExtensions.cs:72), resolves the application name from DataProtection:ApplicationName or the host's ApplicationName (AwsDataProtectionExtensions.cs:74-75), and calls PersistKeysToAWSSystemsManager(prefix, ...) (AwsDataProtectionExtensions.cs:80), setting KMSKeyId only when DataProtection:KmsKeyId is configured (AwsDataProtectionExtensions.cs:82-85).
  • Why it's built this way: each key is one SecureString parameter, so it is KMS-encrypted at rest with no separate key-protection step (AwsDataProtectionExtensions.cs:27-29). The gate keeps developer machines and test hosts on the in-memory default with no AWS dependency at startup (AwsDataProtectionExtensions.cs:33-36). The doc comment states that setting this prefix together with DataProtection:BlobStorageUri fails the startup tripwire (AwsDataProtectionExtensions.cs:52-53), one key ring per host. Recorded in ADR-069 and ADR-134; the Azure sibling is AzureDataProtectionExtensions.
  • Where it's used: no call site outside the defining file; no MMCA.ADC/Source host calls it.
  • Caveats / not-in-source: the required IAM permissions (ssm:GetParametersByPath, ssm:PutParameter, kms:Encrypt, kms:Decrypt) are stated in the doc comment (AwsDataProtectionExtensions.cs:45-49) and are not checked by code.

SnsNativePushSettings

MMCA.Common.Aws · MMCA.Common.Aws.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsNativePushSettings.cs:12 · Level 1 · class (internal sealed)

  • What it is: the bound options for SNS mobile push, section NativePush:Sns (SnsNativePushSettings.cs:15), gated by the shared sibling key NativePush:Enabled (SnsNativePushSettings.cs:18).
  • Depends on: DeviceInstallationRequest for the normalized platform constants.
  • Concept: cross-reference AwsServiceCollectionExtensions (AddAmazonSnsPush) for the gate-plus-completeness registration pattern this class feeds.
  • Walkthrough: FcmPlatformApplicationArn and ApnsPlatformApplicationArn (SnsNativePushSettings.cs:21,24) are the SNS platform applications; BroadcastTopicArn (SnsNativePushSettings.cs:27) is the topic every endpoint subscribes to; InstallationsTable (SnsNativePushSettings.cs:30) is the DynamoDB registry; Region, ServiceUrl, AccessKeyId, SecretAccessKey (SnsNativePushSettings.cs:36,39,45,48) follow the package's shared endpoint and credential rules. IsComplete (SnsNativePushSettings.cs:54-57) requires the table, the topic, and at least one platform application. PlatformApplicationArnFor(platform) (SnsNativePushSettings.cs:62-71) maps DeviceInstallationRequest.FcmV1Platform / ApnsPlatform to the configured ARN, returning null for an unknown platform or a blank ARN.
  • Why it's built this way: allowing one platform application lets a deployment run one platform only (the registrar comment cites an Android-first app with no APNs application, MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsPushDeviceRegistrar.cs:60). The completeness check is what turns an incomplete section into a registration no-op rather than a runtime failure (ADR-070).
  • Where it's used: AddAmazonSnsPush (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:168-180), SnsPushClientFactory, and through IOptions<SnsNativePushSettings> in SnsInstallationRegistry, SnsNativePushSender and SnsPushDeviceRegistrar.

SnsPushMessage

MMCA.Common.Aws · MMCA.Common.Aws.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsPushMessage.cs:12 · Level 1 · class (internal static)

  • What it is: builds the JSON-structured SNS publish body that carries one platform-specific payload per protocol.
  • Depends on: NativePushPayloads for the FCM v1 and APNs payloads; System.Text.Json.
  • Concept: SNS "message structure json": with MessageStructure = "json" (SnsPushMessage.cs:15), SNS reads a dictionary of per-protocol entries and delivers each subscriber its own entry. Reusing the core payload builders keeps the device-side shape identical across the Azure, GCP and AWS push adapters. [Rubric §2, Design Patterns] assesses whether shared logic is reused across adapters; here the vendor adapter only wraps the core payloads.
  • Walkthrough: Build(title, body, metadata) (SnsPushMessage.cs:27) computes the FCM v1 and APNs payloads (SnsPushMessage.cs:29-30) and serializes four entries (SnsPushMessage.cs:32-38): default (the body text), GCM (the FCM payload wrapped in the fcmV1Message envelope), APNS and APNS_SANDBOX (the same APNs payload for both).
  • Why it's built this way: the doc comment records that SNS rejects a JSON-structured publish without a default entry, and that default is what a non-platform subscriber (an operator-added e-mail or SQS subscription) receives (SnsPushMessage.cs:18-21). Duplicating APNs into both keys means one message serves a production or sandbox platform application. See ADR-044.
  • Where it's used: SnsNativePushSender for both targeted and broadcast publishes (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsNativePushSender.cs:42,77-78,94).

AwsSecretsManagerConfigurationExtensions

MMCA.Common.Aws · MMCA.Common.Aws · MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsSecretsManagerConfigurationExtensions.cs:23 · Level 2 · class (static, extension<TBuilder>)

  • What it is: AddAwsSecretsManagerConfiguration(), which adds an AWS Secrets Manager configuration source so every secret under one name prefix reads through IConfiguration like any other setting.
  • Depends on: AwsSecretsManagerConfigurationSource, AwsStaticCredentials, AwsClientEndpoint, HostIntegrationMarker, HostIntegrationNames; AWS SDK AmazonSecretsManagerClient.
  • Concept: runtime secrets as a configuration source, with the same gate-plus-receipt shape as AwsDataProtectionExtensions. [Rubric §11, Security] assesses whether secrets stay out of files and images; here production secrets are fetched from the vault at startup and optionally re-read on an interval.
  • Walkthrough: reads SecretsManager:NamePrefix (AwsSecretsManagerConfigurationExtensions.cs:85) and returns unchanged when blank, registering no receipt (AwsSecretsManagerConfigurationExtensions.cs:87-94). It then resolves the static key pair, failing before any source is added (AwsSecretsManagerConfigurationExtensions.cs:96-98). ReloadIntervalMinutes, when present, must parse as a positive invariant-culture integer or the method throws naming the value (AwsSecretsManagerConfigurationExtensions.cs:103-115). It builds an AmazonSecretsManagerConfig through AwsClientEndpoint.Apply (AwsSecretsManagerConfigurationExtensions.cs:117-121), registers HostIntegrationMarker(HostIntegrationNames.AwsSecretsManagerConfiguration) (AwsSecretsManagerConfigurationExtensions.cs:125), and adds the source with a client factory, the prefix and the interval (AwsSecretsManagerConfigurationExtensions.cs:127-130).
  • Why it's built this way: the naming convention matches the Key Vault sibling (AzureKeyVaultConfigurationExtensions): the prefix is stripped and -- maps to the configuration separator, so adc/prod/ConnectionStrings--Default arrives as ConnectionStrings:Default (AwsSecretsManagerConfigurationExtensions.cs:53-57), letting a secret set move between clouds by renaming its prefix. A malformed reload interval fails loudly rather than degrading to "never reload" (AwsSecretsManagerConfigurationExtensions.cs:100-102). It is deliberately not called from AddServiceDefaults(), and because ConfigurationManager loads each source as it is added the secrets are read synchronously at this call, so it must run before the settings binding that reads them (AwsSecretsManagerConfigurationExtensions.cs:67-73). See ADR-061 and ADR-134.
  • Where it's used: no call site outside the defining file; no MMCA.ADC/Source host calls it.
  • Caveats / not-in-source: the claim that a secret the identity cannot read fails startup (AwsSecretsManagerConfigurationExtensions.cs:63-64) is enforced in the configuration provider, not in this file.

RdsIamAuthenticationConfigurator

MMCA.Common.Aws · MMCA.Common.Aws.Rds · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Rds/RdsIamAuthenticationConfigurator.cs:20 · Level 2 · class (internal sealed)

  • What it is: an INpgsqlDataSourceConfigurator that replaces the PostgreSQL password with a short-lived RDS IAM token signed locally for each connection string's own host, port and user.
  • Depends on: INpgsqlDataSourceConfigurator, ConnectionStringSettings, DataSourcesSettings, RdsIamAuthenticationSettings; Npgsql NpgsqlDataSourceBuilder.UsePeriodicPasswordProvider, AWS SDK RDSAuthTokenGenerator.
  • Concept introduced, token-as-password database auth. Instead of a stored password, the data source asks a periodic provider for a SigV4-signed token with a 15-minute lifetime (RdsIamAuthenticationConfigurator.cs:12-15). Because the token is a bearer credential sent as the password, the configurator refuses any connection string that does not require TLS. [Rubric §11, Security] assesses credential handling; here no database password exists to leak, and the transport is forced encrypted. [Rubric §7, Microservices Readiness] is also relevant: it plugs into the core data-source configurator, so every context and the readiness check share one data source per connection string (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:91-93).
  • Walkthrough: primary-constructor parameters region and credentials, both nullable (RdsIamAuthenticationConfigurator.cs:20). TokenRefreshInterval is 10 minutes (RdsIamAuthenticationConfigurator.cs:24) and FailureRetryInterval 30 seconds (RdsIamAuthenticationConfigurator.cs:27). Configure(builder) (RdsIamAuthenticationConfigurator.cs:31) checks TLS on the builder's connection string (RdsIamAuthenticationConfigurator.cs:35-37) and installs UsePeriodicPasswordProvider(GenerateTokenAsync, ...) (RdsIamAuthenticationConfigurator.cs:39). EnsureConfiguredConnectionStringsAreEncrypted(configuration) (RdsIamAuthenticationConfigurator.cs:50) checks the top-level ConnectionStrings:PostgreSQLConnectionString and every DataSources:{name}:PostgreSQLConnectionString already in configuration (RdsIamAuthenticationConfigurator.cs:54-59), parsing each through NpgsqlDataSourceBuilder so it sees what Configure will see (RdsIamAuthenticationConfigurator.cs:65-66). GenerateTokenAsync (RdsIamAuthenticationConfigurator.cs:75) requires a Host and Username (RdsIamAuthenticationConfigurator.cs:82-86), then picks one of four RDSAuthTokenGenerator.GenerateAuthTokenAsync overloads by which of credentials and region are set (RdsIamAuthenticationConfigurator.cs:90-105). EnsureEncrypted (RdsIamAuthenticationConfigurator.cs:108) accepts Require, VerifyCA or VerifyFull (RdsIamAuthenticationConfigurator.cs:110) and otherwise throws a message naming the source and the fix (RdsIamAuthenticationConfigurator.cs:115-118).
  • Why it's built this way: refreshing every 10 minutes keeps the current token well inside its 15-minute lifetime (RdsIamAuthenticationConfigurator.cs:23). Checking configured connection strings at registration makes a cleartext misconfiguration fail at startup, while Configure still catches a connection string supplied later (ADR-070). See ADR-113 and ADR-134.
  • Where it's used: constructed and registered by AddAwsRdsIamAuthentication (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:124,128); exercised by MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Rds/AwsRdsIamAuthenticationTests.cs.
  • Caveats / not-in-source: SQL Server, SQLite and Cosmos sources are untouched (AwsServiceCollectionExtensions.cs:98-99), since this only implements the Npgsql configurator.

SnsPushClientFactory

MMCA.Common.Aws · MMCA.Common.Aws.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsPushClientFactory.cs:12 · Level 2 · class (internal static)

  • What it is: builds the SNS and DynamoDB clients the SNS push adapter needs, from one SnsNativePushSettings and optional static credentials.
  • Depends on: SnsNativePushSettings, AwsClientEndpoint; AWS SDK SNS and DynamoDB clients.
  • Concept: cross-reference AwsClientEndpoint; the same factory shape as S3StorageClientFactory.
  • Walkthrough: CreateSns (SnsPushClientFactory.cs:18) and CreateDynamoDb (SnsPushClientFactory.cs:34) are symmetric: a fresh config, AwsClientEndpoint.Apply(config, settings.ServiceUrl, settings.Region) (SnsPushClientFactory.cs:23,39), then the client with or without credentials (SnsPushClientFactory.cs:25-27,41-43). One ServiceUrl therefore redirects both services to the same emulator.
  • Where it's used: the TryAddSingleton factories in AddAmazonSnsPush (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:181-182), so a host-registered client wins.

AmazonSqsTransport

MMCA.Common.Aws · MMCA.Common.Aws.Messaging · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Messaging/AmazonSqsTransport.cs:37 · Level 4 · class (internal sealed)

  • What it is: the Amazon SQS/SNS IBrokerTransport, selected by MessageBus:Provider=AmazonSqs (AmazonSqsTransport.cs:40). MassTransit publishes to one SNS topic per message type and each receive endpoint is an SQS queue subscribed to it (AmazonSqsTransport.cs:14-16).
  • Depends on: IBrokerTransport, BrokerTransportContext, BrokerTransportPolicies, MessageBusSettings, AmazonSqsTransportSettings, AwsStaticCredentials; MassTransit UsingAmazonSqs, AWS SDK RegionEndpoint.
  • Concept: a pluggable transport strategy; cross-reference AzureServiceBusTransport for the shared shape. [Rubric §10, Messaging & Integration] assesses broker portability and poison-message handling; this transport keeps the core receive policies but adapts redelivery to an SQS limit. [Rubric §7, Microservices Readiness] also applies: the vendor transport lives only in this adapter package, and the cloud-vendor lists in ArchitectureRules forbid MassTransit.AmazonSqsTransport and MMCA.Common.Aws from cloud-neutral assemblies (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.CloudVendors.cs:36,38).
  • Walkthrough: constants MaxDelaySeconds = 900 (AmazonSqsTransport.cs:43) and AmbientHeaderPrefix = "MMCA-" (AmazonSqsTransport.cs:46); DefaultRedeliveryIntervalsSeconds = [60, 300, 900] (AmazonSqsTransport.cs:55). The public constructor falls back to FallbackRegionFactory.GetRegionEndpoint (AmazonSqsTransport.cs:58-61); an internal one accepts a test fallback (AmazonSqsTransport.cs:65-66). Configure (AmazonSqsTransport.cs:72) binds the settings (AmazonSqsTransport.cs:78-79) and resolves intervals, region, credentials and endpoint before the bus-factory callback, so every configuration error surfaces while AddBrokerMessaging runs (AmazonSqsTransport.cs:81-88). Inside UsingAmazonSqs it sets the host region, the credentials when present, an SQS and an SNS config with the endpoint override and signing region (AmazonSqsTransport.cs:92-104), the header filter (AmazonSqsTransport.cs:106), then BrokerTransportPolicies.ApplyReceivePolicies and ConfigureEndpoints (AmazonSqsTransport.cs:109-110). ResolveRedeliveryIntervals (AmazonSqsTransport.cs:124) defers to BrokerTransportPolicies.BuildRedeliveryIntervals with the SQS default and then EnsureWithinDelayCap (AmazonSqsTransport.cs:149), which throws for any interval over 900 seconds (AmazonSqsTransport.cs:153-161). IsTransportHeader (AmazonSqsTransport.cs:139-140) excludes MMCA-* headers. ResolveRegion (AmazonSqsTransport.cs:172) uses the configured name, else the fallback, else throws naming the key (AmazonSqsTransport.cs:176-183).
  • Why it's built this way: SQS caps a delay at 15 minutes and MassTransit silently shortens a longer one, so the framework default (which has a one-hour step) is replaced by [60, 300, 900] when unconfigured, while an explicit over-cap interval fails registration (AmazonSqsTransport.cs:18-26). Delayed redelivery is unconditional because SQS delays natively, so MessageBus:EnableDelayedRedelivery (a RabbitMQ plugin gate) is not consulted (AmazonSqsTransport.cs:18-21). The MMCA-* ambient headers stay out of message attributes because SQS and SNS reject more than 10 attributes; they still travel in the MassTransit envelope body (AmazonSqsTransport.cs:28-33). See ADR-066, ADR-087 and ADR-134.
  • Where it's used: registered by AddAmazonSqsTransport (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:85-86); covered by MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Messaging/AmazonSqsTransportTests.cs.

SnsInstallationRegistry

MMCA.Common.Aws · MMCA.Common.Aws.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsInstallationRegistry.cs:20 · Level 5 · class (internal sealed)

  • What it is: the DynamoDB-backed table of push installations (one item per installation id) plus the SNS endpoint-removal helper, used because SNS keeps no owner per endpoint.
  • Depends on: SnsInstallation, SnsNativePushSettings; AWS SDK IAmazonDynamoDB and IAmazonSimpleNotificationService.
  • Concept introduced, ownership rules as conditional writes. The IPushDeviceRegistrar contract says an installation id may be claimed when it is new, already the caller's, or another user's whose current push channel the caller presents (the same device changing hands). Rather than read-then-write, the rule is one DynamoDB ConditionExpression, OwnershipCondition (SnsInstallationRegistry.cs:36), so a race between two writers cannot both win. [Rubric §8, Data Architecture] assesses consistency under concurrency; here the guarantee is atomic at the store. [Rubric §11, Security] applies too: a user cannot hijack another device's installation.
  • Walkthrough: UserIdIndexName = "UserId-index" (SnsInstallationRegistry.cs:29) names the GSI used for user-targeted sends. UserKey (SnsInstallationRegistry.cs:50-51) is the invariant-culture string form of a user id. GetAsync (SnsInstallationRegistry.cs:57) reads with ConsistentRead = true (SnsInstallationRegistry.cs:64) because ownership decisions follow. TryPutAsync (SnsInstallationRegistry.cs:75) writes all attributes (omitting a blank SubscriptionArn, SnsInstallationRegistry.cs:85-88) under the ownership condition (SnsInstallationRegistry.cs:97) and returns false on ConditionalCheckFailedException (SnsInstallationRegistry.cs:107-110). QueryByUserAsync (SnsInstallationRegistry.cs:117) pages the GSI until LastEvaluatedKey is empty (SnsInstallationRegistry.cs:145-150). DeleteOwnedAsync (SnsInstallationRegistry.cs:162) and DeleteIfEndpointAsync (SnsInstallationRegistry.cs:173) are conditional deletes through DeleteWhenAsync (SnsInstallationRegistry.cs:201), which swallows a failed condition because the row has changed hands or endpoints (SnsInstallationRegistry.cs:218-221). RemoveEndpointAsync (SnsInstallationRegistry.cs:184) unsubscribes first, treating NotFoundException as done (SnsInstallationRegistry.cs:190-195), then deletes the endpoint (SnsInstallationRegistry.cs:198). FromItem (SnsInstallationRegistry.cs:227) returns null for a row missing the id, user or endpoint (SnsInstallationRegistry.cs:232-236).
  • Why it's built this way: the conditional deletes make cleanup safe against interleaving: an owner delete leaves a row that was handed over (SnsInstallationRegistry.cs:155-156), and a stale-endpoint cleanup leaves a row refreshed onto a new endpoint (SnsInstallationRegistry.cs:166-167). See ADR-044.
  • Where it's used: registered as a singleton in AddAmazonSnsPush (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:183); consumed by SnsNativePushSender and SnsPushDeviceRegistrar.
  • Caveats / not-in-source: the table and its UserId-index GSI are provisioned outside the application (AwsServiceCollectionExtensions.cs:150-152); nothing here creates or validates them.

SnsNativePushSender

MMCA.Common.Aws · MMCA.Common.Aws.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsNativePushSender.cs:31 · Level 6 · class (internal sealed partial)

  • What it is: the SNS implementation of INativePushSender: per-user sends publish to each of the user's endpoints, and a broadcast is one publish to the broadcast topic.
  • Depends on: INativePushSender, SnsInstallationRegistry, SnsInstallation, SnsPushMessage, SnsNativePushSettings; AWS SDK SNS client, LoggerMessage source generation.
  • Concept: cross-reference SnsInstallationRegistry. [Rubric §29, Resilience & Business Continuity] assesses partial-failure behavior; here a lookup failure skips only that user and a publish failure skips only that endpoint.
  • Walkthrough: SendToUsersAsync (SnsNativePushSender.cs:38) builds the message once (SnsNativePushSender.cs:42), de-duplicates user keys (SnsNativePushSender.cs:45), queries each user's installations and logs-and-continues on AmazonServiceException (SnsNativePushSender.cs:50-56), groups rows by EndpointArn so two installation ids sharing one endpoint get one publish (SnsNativePushSender.cs:58), and counts successful publishes (SnsNativePushSender.cs:60-63,67). BroadcastAsync (SnsNativePushSender.cs:71) publishes to BroadcastTopicArn (SnsNativePushSender.cs:76). TryPublishAsync (SnsNativePushSender.cs:85) targets the endpoint (SnsNativePushSender.cs:92); EndpointDisabledException or NotFoundException triggers stale cleanup (SnsNativePushSender.cs:99-102) and any other AmazonServiceException is logged (SnsNativePushSender.cs:104-108). RemoveStaleAsync (SnsNativePushSender.cs:115) removes the endpoint once and then every row pointing at it with the endpoint-conditional delete (SnsNativePushSender.cs:120-124). Six source-generated log methods follow (SnsNativePushSender.cs:132-148).
  • Why it's built this way: a broadcast through a topic subscription costs one SNS call regardless of device count, while user-targeted sends need the registry's GSI. Removing every row for a stale endpoint means no surviving row fails its own publish later (SnsNativePushSender.cs:111-113). See ADR-044.
  • Where it's used: registered transient as INativePushSender by AddAmazonSnsPush (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:184), replacing NullNativePushSender; covered by MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Notifications/SnsNativePushSenderTests.cs.
  • Caveats / not-in-source: BroadcastAsync has no try/catch, so an SNS failure there propagates to the caller (SnsNativePushSender.cs:71-83); how callers handle it is outside this file.

SnsPushDeviceRegistrar

MMCA.Common.Aws · MMCA.Common.Aws.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsPushDeviceRegistrar.cs:32 · Level 6 · class (internal sealed)

  • What it is: the SNS implementation of IPushDeviceRegistrar: it maps a device installation onto an SNS platform endpoint, subscribes that endpoint to the broadcast topic, and records ownership in the DynamoDB registry.
  • Depends on: IPushDeviceRegistrar, DeviceInstallationRequest, Result, Error, SnsInstallationRegistry, SnsInstallation, SnsNativePushSettings; AWS SDK SNS client.
  • Concept: cross-reference SnsInstallationRegistry for the ownership rule, and AzureNotificationHubDeviceRegistrar, whose rule this one states it mirrors (SnsPushDeviceRegistrar.cs:129-130). Errors come back as Result failures, not exceptions. [Rubric §29, Resilience & Business Continuity] assesses compensation on partial failure; the registrar orders and undoes SNS side effects so no endpoint is left subscribed without a row.
  • Walkthrough: UpsertAsync (SnsPushDeviceRegistrar.cs:39) normalizes fcmv1 / apns case-insensitively (SnsPushDeviceRegistrar.cs:43-48), returns PushDevice.UnsupportedPlatform validation failures for an unknown platform (SnsPushDeviceRegistrar.cs:51-54) or one without a configured platform application (SnsPushDeviceRegistrar.cs:57-64), and maps AmazonServiceException to PushDevice.UpsertFailed (SnsPushDeviceRegistrar.cs:71-77). RegisterAsync (SnsPushDeviceRegistrar.cs:139) reads the row, returns PushDevice.InstallationConflict (SnsPushDeviceRegistrar.cs:122-126) when ClaimedByAnotherDevice is true (SnsPushDeviceRegistrar.cs:134-137,146-149), ensures an endpoint (SnsPushDeviceRegistrar.cs:151), reuses the existing subscription only when the endpoint is unchanged, else subscribes with protocol application (SnsPushDeviceRegistrar.cs:153-168), and writes the row with TryPutAsync (SnsPushDeviceRegistrar.cs:170-171). A refused write undoes only what this request created (SnsPushDeviceRegistrar.cs:178-182); a row that moved endpoints has its old endpoint removed (SnsPushDeviceRegistrar.cs:185-190). EnsureEndpointAsync (SnsPushDeviceRegistrar.cs:200) reuses a same-platform endpoint via TryRefreshEndpointAsync, which sets Token and Enabled = "true" and returns false on NotFoundException (SnsPushDeviceRegistrar.cs:221-242); otherwise it calls CreatePlatformEndpoint (SnsPushDeviceRegistrar.cs:210-213). DeleteAsync (SnsPushDeviceRegistrar.cs:82) returns success for an unknown or foreign id (SnsPushDeviceRegistrar.cs:87-93), removes the endpoint only when no other row of the same user shares it (SnsPushDeviceRegistrar.cs:100-107), then deletes the row conditionally on the owner (SnsPushDeviceRegistrar.cs:109); failures map to PushDevice.DeleteFailed (SnsPushDeviceRegistrar.cs:112-118). RemoveUnaccountedAsync (SnsPushDeviceRegistrar.cs:245) is best-effort and only logs on failure (SnsPushDeviceRegistrar.cs:258-262).
  • Why it's built this way: the conflict check runs before anything is created so the other device's endpoint and row stay untouched, and the conditional write repeats it atomically for a row that appears in between (SnsPushDeviceRegistrar.cs:143-145). Delete runs endpoint first, row last, so a partial failure leaves a row the client's retry can find (SnsPushDeviceRegistrar.cs:95-99). Reporting success for a foreign id avoids an existence oracle (SnsPushDeviceRegistrar.cs:90-91). CreatePlatformEndpoint is idempotent per token, so a retried registration does not multiply endpoints (SnsPushDeviceRegistrar.cs:197-198). See ADR-044.
  • Where it's used: registered transient as IPushDeviceRegistrar by AddAmazonSnsPush (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:185), replacing NullPushDeviceRegistrar; covered by MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Notifications/SnsPushDeviceRegistrarTests.cs.
  • Caveats / not-in-source: the source names a known edge: when the conditional write is refused and CreatePlatformEndpoint had returned an existing endpoint for a token registered under another installation id, the undo removes that endpoint too; the other row's next send finds it gone and drops it, and its device re-registers on start (SnsPushDeviceRegistrar.cs:175-177).

AwsServiceCollectionExtensions

MMCA.Common.Aws · MMCA.Common.Aws · MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:29 · Level 7 · class (static, extension(IServiceCollection))

  • What it is: the public DI surface of the AWS adapter package: AddS3FileStorage, AddAmazonSqsTransport, AddAwsRdsIamAuthentication and AddAmazonSnsPush, each replacing a provider-neutral core default with an AWS implementation.
  • Depends on: S3FileStorageSettings, S3StorageClientFactory, S3FileStorageService, IFileStorageService, AmazonSqsTransport, RdsIamAuthenticationSettings, RdsIamAuthenticationConfigurator, SnsNativePushSettings, SnsPushClientFactory, SnsInstallationRegistry, SnsNativePushSender, SnsPushDeviceRegistrar, INativePushSender, IPushDeviceRegistrar, AwsStaticCredentials.
  • Concept introduced, the cloud-neutral core with vendor adapters. Core Common defines the abstractions and a safe default (an unconfigured file store, a null push sender, the broker transport registry, the Npgsql configurator hook); this package swaps in AWS implementations only when called and only when configured. Every method is configuration-gated, so a host can call it unconditionally and a deployment turns the capability on by configuration alone. Declared as a C# extension(IServiceCollection) block (AwsServiceCollectionExtensions.cs:31; see C# extension(T) types, ADR-106). [Rubric §7, Microservices Readiness] assesses vendor independence of the core; ADR-134 records the split, and ArchitectureRules.CloudVendors enforces it (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.CloudVendors.cs:96-97). [Rubric §1, SOLID] (open/closed): new providers arrive as packages, not core edits. Sibling surfaces: AzureServiceCollectionExtensions, GcpServiceCollectionExtensions.
  • Walkthrough:
    • AddS3FileStorage(configuration) (AwsServiceCollectionExtensions.cs:50) binds FileStorage options (AwsServiceCollectionExtensions.cs:54-55), is a no-op without a Bucket (AwsServiceCollectionExtensions.cs:57-61), checks the key pair at registration (AwsServiceCollectionExtensions.cs:64), then TryAddSingletons the S3 client and adds IFileStorageService as S3FileStorageService (AwsServiceCollectionExtensions.cs:66-67). Checksum settings default to WHEN_REQUIRED because SDK v4 sends integrity checksums by default and several S3-compatible services reject them (AwsServiceCollectionExtensions.cs:41-42). See ADR-045.
    • AddAmazonSqsTransport() (AwsServiceCollectionExtensions.cs:85-86) registers a new AmazonSqsTransport through AddBrokerTransport; it must be called before AddBrokerMessaging, which picks the transport (AwsServiceCollectionExtensions.cs:74-75). See ADR-066.
    • AddAwsRdsIamAuthentication(configuration) (AwsServiceCollectionExtensions.cs:107) is a no-op unless Rds:IamAuthentication:Enabled is true (AwsServiceCollectionExtensions.cs:111-115), resolves credentials (AwsServiceCollectionExtensions.cs:117-120), fails on any configured cleartext PostgreSQL connection string (AwsServiceCollectionExtensions.cs:122-124), resolves the optional region (AwsServiceCollectionExtensions.cs:126) and calls AddNpgsqlDataSourceConfigurator with a RdsIamAuthenticationConfigurator (AwsServiceCollectionExtensions.cs:128).
    • AddAmazonSnsPush(configuration) (AwsServiceCollectionExtensions.cs:164) is a no-op unless NativePush:Enabled parses true and the section IsComplete (AwsServiceCollectionExtensions.cs:168-175); it then resolves credentials (AwsServiceCollectionExtensions.cs:178), binds options (AwsServiceCollectionExtensions.cs:180), TryAddSingletons the SNS and DynamoDB clients and the registry (AwsServiceCollectionExtensions.cs:181-183), and adds the sender and registrar as transients (AwsServiceCollectionExtensions.cs:184-185). See ADR-044.
  • Why it's built this way: TryAddSingleton for SDK clients means a client the host registered first is used as is, the same posture as the Azure adapter (AwsServiceCollectionExtensions.cs:44-45,157-158). Every key-pair check runs at registration, not on first use (ADR-070). The configuration-pipeline integrations (secrets, data protection) live on the host builder instead, in AwsSecretsManagerConfigurationExtensions and AwsDataProtectionExtensions, because they act on builder.Configuration.
  • Where it's used: no call site outside the defining file; no MMCA.ADC/Source host calls any of the four methods (ADC runs on Azure). Tests call them, for example MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Notifications/AddAmazonSnsPushTests.cs.
  • Caveats / not-in-source: the IAM permissions listed for SNS push (AwsServiceCollectionExtensions.cs:150-158) and the external provisioning of the table, topic and platform applications are documentation only; no code verifies them.

AwsSecretsManagerConfigurationProvider

MMCA.Common.Aws · MMCA.Common.Aws.Secrets · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Secrets/AwsSecretsManagerConfigurationProvider.cs:27 · Level 0 · class (internal sealed)

  • What it is: the ConfigurationProvider that reads every AWS Secrets Manager secret under one name prefix into IConfiguration, mapping names the same way the Key Vault provider does so one secret-name convention serves both clouds: the prefix is stripped and -- becomes the configuration separator, so adc/prod/ConnectionStrings--Default arrives as ConnectionStrings:Default (AwsSecretsManagerConfigurationProvider.cs:8-12).
  • Depends on: nothing first-party at runtime; it is built by AwsSecretsManagerConfigurationSource. Externals: Amazon.SecretsManager (IAmazonSecretsManager, ListSecretsRequest, BatchGetSecretValueRequest), Microsoft.Extensions.Configuration (ConfigurationProvider, ConfigurationPath), and BCL PeriodicTimer / CancellationTokenSource (AwsSecretsManagerConfigurationProvider.cs:1-3).
  • Concept introduced, secrets as a configuration source with a fail-loud first load and a fail-soft reload. [Rubric §11, Security] assesses whether secrets live outside code and deployment artifacts and reach the process through an identity-scoped channel: here they are read from Secrets Manager at startup, and the error a missing grant raises names the exact IAM actions to grant (:183-188). [Rubric §29, Resilience & Business Continuity] assesses whether a dependency failure degrades rather than takes down a running system: the first load must succeed (a secret that cannot be read fails startup, naming it, :20-22), but a failed timed reload keeps the values already loaded and records the exception in LastReloadFailure instead of blanking settings the host is using (:124-129). [Rubric §33, Developer Experience] applies through the shared naming convention: a team moving between Azure and AWS keeps one secret-name scheme.
  • Walkthrough:
    • Constants and state. BatchSize = 20 is the most ids one BatchGetSecretValue call accepts (:30); KeyDelimiter is "--" (:32). The client, prefix, optional reload interval, a disposal CancellationTokenSource and the polling task are the fields (:34-38). The constructor null-guards the client and prefix (:44-49).
    • KeyFor(secretName, namePrefix) (:61-69) returns null when the name is outside the prefix (an ordinal StartsWith) or equals the prefix exactly, otherwise the suffix with every -- replaced by ConfigurationPath.KeyDelimiter.
    • Load() (:72-86) blocks once on LoadAsync, under a scoped VSTHRD002 suppression (:78-80). The comment explains why the wait is safe: ConfigurationProvider.Load is synchronous by contract, the AWS SDK v4 client is async only, and no synchronization context is held while the configuration manager adds a source (:74-77). When a reload interval is set and no poller exists yet, it starts PollAsync (:82-85).
    • LoadAsync (:132-143) lists names, then reads them in chunks of BatchSize into a Dictionary keyed with StringComparer.OrdinalIgnoreCase (:135, :137).
    • ListNamesAsync (:145-172) pages ListSecrets with a Name filter on the prefix (:154) and re-checks every name through KeyFor (:161). The re-check matters because the service filter is a case-insensitive prefix match, so a secret for another environment in a different case would otherwise slip in (:13-17).
    • ReadBatchAsync (:174-202) pages BatchGetSecretValue. Any per-secret error throws an InvalidOperationException that lists each secret id, error code and message and states the fix: grant secretsmanager:GetSecretValue (and kms:Decrypt on a customer-managed key) or move the secret out of the prefix (:183-188). Only SecretString values are kept, so a binary-only secret is skipped (:191-196).
    • PollAsync / ReloadOnceAsync (:96-110, :112-130) tick a PeriodicTimer; each tick reloads, clears LastReloadFailure, and only when HasSameData (:204-206) reports a difference swaps Data and calls OnReload(), so an unchanged secret set fires no change token. Any non-cancellation exception is stored, not thrown (:124-129).
    • Dispose() (:89-94) cancels the poller and disposes the token source and the client the provider owns.
  • Why it's built this way: ADR-061 records runtime secret management, and mirroring the Key Vault provider's name mapping and reload posture is what keeps the AWS adapter a drop-in sibling rather than a second convention. Re-checking the prefix ordinally and comparing whole data sets before OnReload keep the provider from importing a wrong-environment secret or churning every IOptionsMonitor on each tick.
  • Where it's used: constructed only by AwsSecretsManagerConfigurationSource.Build (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Secrets/AwsSecretsManagerConfigurationSource.cs:26-27), which AwsSecretsManagerConfigurationExtensions adds. Covered by MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Secrets/AwsSecretsManagerConfigurationProviderTests.cs.
  • Caveats / not-in-source: no host in MMCA.ADC/Source calls AddAwsSecretsManagerConfiguration, so which deployments read secrets this way is Not determinable from source here.

AzureBlobFileStorageSettings

MMCA.Common.Azure · MMCA.Common.Azure.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Storage/AzureBlobFileStorageSettings.cs:10 · Level 0 · class (public sealed)

  • What it is: the options class bound from the FileStorage section for the Azure Blob adapter: a blob service endpoint, a connection-string alternative, and the one container every blob lives in (AzureBlobFileStorageSettings.cs:10-23).
  • Depends on: nothing first-party; System.Uri only.
  • Concept: a plain bound-options class; the fail-fast configuration contract it participates in is recorded in ADR-070. [Rubric §11, Security] assesses whether production authenticates with an identity rather than a shared key: ServiceUri is the managed-identity path (DefaultAzureCredential), and ConnectionString is documented as the local-development (Azurite) alternative (:15-19).
  • Walkthrough: SectionName is "FileStorage" (:13); ServiceUri (:16), ConnectionString (:19) and ContainerName (:22, documented as required) are init-only.
  • Why it's built this way: one section with two mutually-exclusive credential shapes lets the same host code run against Azurite locally and a managed identity in Azure. It shares the FileStorage section name with S3FileStorageSettings, so a host picks its vendor by which registration it calls, not by renaming configuration (ADR-045).
  • Where it's used: AzureServiceCollectionExtensions.AddAzureBlobFileStorage binds it (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:45-48) and registers nothing when ContainerName is blank or neither an absolute ServiceUri nor a ConnectionString is present (:49-59); the comment at :54 records the trap that an empty-string ServiceUri binds to a relative Uri, so only IsAbsoluteUri counts.

AzureNotificationHubSettings

MMCA.Common.Azure · MMCA.Common.Azure.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Notifications/AzureNotificationHubSettings.cs:9 · Level 0 · class (public sealed)

  • What it is: the options class bound from the NativePush section that switches Azure Notification Hubs delivery on: an Enabled flag, a connection string and a hub name (AzureNotificationHubSettings.cs:9-22).
  • Depends on: nothing first-party.
  • Concept: configuration as the on switch. [Rubric §17, DevOps] assesses whether a capability can be enabled per environment without a code change: the host registers the channel unconditionally and the section decides (see ADR-044, ADR-070).
  • Walkthrough: SectionName is "NativePush" (:12); Enabled (:15), ConnectionString (:18, documented as a Listen+Send+Manage rule) and HubName (:21) are init-only.
  • Why it's built this way: the registration doc says the hub is provisioned before its FCM/APNs credentials exist, so a deployment needs to hold the section and keep the channel off until the platform credentials are in place (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:73-80).
  • Where it's used: AddAzureNotificationHubsPush binds it (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:87-90) and returns without registering anything unless Enabled is true and both strings are non-blank (:91-96).

NativePushUserTags

MMCA.Common.Azure · MMCA.Common.Azure.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Notifications/NativePushUserTags.cs:8 · Level 0 · class (internal static)

  • What it is: the owner-tag vocabulary of the Azure Notification Hubs adapter: how a user id becomes a user:{id} installation tag, and how a set of users becomes the OR tag expressions a send targets (NativePushUserTags.cs:8-29).
  • Depends on: the UserIdentifierType alias; BCL string.Create with CultureInfo.InvariantCulture.
  • Concept introduced, addressing users, not devices. [Rubric §10, Messaging & Integration] assesses whether an integration hides vendor addressing behind application concepts: callers send to user ids, and this type translates them into the hub's tag language. [Rubric §12, Performance & Scalability] applies through the chunking: Azure Notification Hubs caps a tag expression at 20 tags, so a large audience becomes several expressions rather than one rejected send (:10-11).
  • Walkthrough: MaxTagsPerExpression = 20 (:11). BuildUserTagExpressions maps every id through UserTag, chunks by 20, and joins each chunk with " || " (:17-21). UserTagPrefix is "user:" (:24), and UserTag formats the id invariantly behind that prefix (:27-28), so the tag stamped at registration and the tag targeted at send time are byte-identical.
  • Why it's built this way: one owner of the tag format keeps the registrar and the sender from drifting apart; a different format on either side would silently deliver to nobody. ADR-044 records the hub as the device registry this tagging relies on.
  • Where it's used: AzureNotificationHubNativePushSender loops BuildUserTagExpressions (MMCA.Common/Source/Hosting/MMCA.Common.Azure/Notifications/AzureNotificationHubNativePushSender.cs:25); AzureNotificationHubDeviceRegistrar stamps UserTag on each installation and uses UserTagPrefix to recognise another owner (MMCA.Common/Source/Hosting/MMCA.Common.Azure/Notifications/AzureNotificationHubDeviceRegistrar.cs:41, :125-128). Covered by MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/Notifications/NativePushUserTagsTests.cs.

S3FileStorageSettings

MMCA.Common.Aws · MMCA.Common.Aws.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Storage/S3FileStorageSettings.cs:13 · Level 0 · class (public sealed)

  • What it is: the options class bound from the FileStorage section for the S3-compatible adapter, one shape that targets AWS S3, Cloudflare R2, MinIO, LocalStack or the GCS XML API (S3FileStorageSettings.cs:13-73).
  • Depends on: nothing first-party. Externals: Amazon.Runtime (RequestChecksumCalculation, ResponseChecksumValidation).
  • Concept introduced, one client, many S3-compatible vendors. [Rubric §32, Dependency & Supply-Chain] assesses whether a vendor dependency is isolated and its defaults are understood rather than inherited: both checksum properties default to WHEN_REQUIRED instead of the AWS SDK v4 default WHEN_SUPPORTED, because the v4 SDK attaches CRC32 trailers and x-amz-checksum-* headers that R2, older MinIO releases and the GCS XML API reject (:58-72). [Rubric §31, Cost & FinOps] is served by the same portability: the bucket vendor is a configuration value.
  • Walkthrough: SectionName is "FileStorage" (:16). Bucket is required and provisioned by infrastructure (:19). Region is the AWS region, or the signing region when ServiceUrl is set (R2 expects auto), and falls back to the SDK chain when blank (:21-26). ServiceUrl selects an S3-compatible endpoint (:28-33); ForcePathStyle switches to {endpoint}/{bucket}/{key} addressing, needed by MinIO and LocalStack (:35-39). PublicBaseUrl overrides the returned URL, for a CDN or R2 public domain (:41-46). AccessKeyId / SecretAccessKey are an optional static pair; setting only one fails registration (:48-56). RequestChecksumCalculation and ResponseChecksumValidation default to WHEN_REQUIRED (:65, :72).
  • Why it's built this way: ADR-134 keeps the core cloud-neutral with vendor adapters beside it, and ADR-045 records the storage contract. Sharing the FileStorage section name with AzureBlobFileStorageSettings means a host switches vendor by its registration call alone.
  • Where it's used: bound by AddS3FileStorage (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:54-57), which registers nothing when Bucket is blank (:58-61) and resolves the key pair at registration (:64). Read by S3StorageClientFactory, S3ObjectUrl and S3FileStorageService; tested across MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Storage/.

ServiceBusEmulatorSupport

MMCA.Common.Azure · MMCA.Common.Azure.Messaging · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Messaging/ServiceBusEmulatorSupport.cs:32 · Level 0 · class (internal, static)

  • What it is: everything the Azure Service Bus transport needs in order to run against the official local emulator instead of a real namespace, collected in one type so the production branch in AzureServiceBusTransport stays a single unconditional cfg.Host(connectionString) (ServiceBusEmulatorSupport.cs:8-11). It answers two questions: is this connection string an emulator one, and if so, how do we hand MassTransit the two clients it needs.

  • Depends on: nothing first-party. Externals: Azure.Messaging.ServiceBus (ServiceBusClient), Azure.Messaging.ServiceBus.Administration (ServiceBusAdministrationClient), and MassTransit's IServiceBusBusFactoryConfigurator (ServiceBusEmulatorSupport.cs:1-4). BCL: Uri, Interlocked, CultureInfo. Its one caller is AzureServiceBusTransport.Configure, and its input comes from MessageBusSettings (ConnectionString plus EmulatorAdminEndpoint).

  • Concept introduced, detection that keys off the artifact rather than the environment. [Rubric §17, DevOps] assesses whether development and production run the same paths: the point of the emulator branch is that a local stack exercises the same transport production uses, instead of substituting RabbitMQ and discovering Service Bus specific behavior only after deployment. [Rubric §11, Security] assesses blast radius: the branch is entered only when the resolved connection string carries UseDevelopmentEmulator=true, a token a real namespace never emits, so no production deployment can reach any of this by accident. That is stated as the reason detection is not keyed off an environment name or a separate flag, either of which somebody can set in the wrong place (ServiceBusEmulatorSupport.cs:12-18). [Rubric §32, Dependency & Supply-Chain]: the whole type exists because of a pinned dependency version. MassTransit v8 has no vendor emulator mode (that shipped in v9, which the workspace excludes because it needs a commercial license), so the only way onto the emulator is the custom-clients Host overload, where the caller builds both the data-plane and the management-plane client itself (ServiceBusEmulatorSupport.cs:19-30).

  • Walkthrough:

    • The four pieces of state. EmulatorMarker is the literal "UseDevelopmentEmulator=true" (ServiceBusEmulatorSupport.cs:37). EmulatorEntityQuota is one hour, the ceiling the emulator enforces on entity time-to-live and auto-delete-on-idle (:42). EmulatorHostAddress is sb://localhost/ (:48) and the doc is careful about what it is: it names the bus for the Host overload, not a network location, because both clients are already bound to the emulator's actual ports (:44-47). _entityQuotasApplied is the once-per-process latch (:54).
    • IsEmulatorConnectionString (:62-64) is the whole detection: a null-guard and a Contains with StringComparison.OrdinalIgnoreCase, so casing in the connection string cannot smuggle a stack onto the wrong branch.
    • ConfigureEmulatorHost (:86-101) is the entry point. It lowers the process-global quotas (:93), derives the management-plane connection string (:95), and calls the custom-clients overload with the host address, a new ServiceBusClient for the data plane and a new ServiceBusAdministrationClient for the management plane (:97-100). The CA2000 suppression above it (:82-85) is worth reading rather than skipping: MassTransit takes ownership of the client for the life of the bus, which is the life of the process, so disposing it here would close the connection before the first publish.
    • ApplyEmulatorEntityQuotas (:108-118) flips Interlocked.Exchange(ref _entityQuotasApplied, 1) != 0 and returns early on the second call (:110-113), then writes three MassTransit statics down to the one-hour quota: DefaultMessageTimeToLive, BasicMessageTimeToLive and AutoDeleteOnIdle (:115-117). MassTransit v8's own defaults sit far above the emulator's ceiling (366 days TTL, 427 days auto-delete), so without this every entity it tries to provision is rejected (:26-29). Because these are process-global statics, the method is deliberately never reached on the real-namespace path: a production bus keeps MassTransit's defaults (:103-107).
    • BuildAdminConnectionString (:131-156) derives the management-plane string from the AMQP one by swapping in the admin endpoint's host and port. The validation is stricter than "is it an absolute URI" and says why in a comment (:133-135): localhost:5300 is an absolute URI, with localhost read as the scheme and no host at all, so the check also requires the scheme to be http or https (:136-139). A missing or malformed endpoint throws an InvalidOperationException whose message names the setting, the reason (v8 needs a management client on a second port) and the fix, including that an Aspire AppHost gets it for free (:141-142). The rebuild itself is a split on ; that replaces only the Endpoint= segment and leaves everything else in place (:145-155), which is what keeps the shared-access key name and value byte-identical across both clients: composing a fresh second string is one silent typo away from an admin client that cannot provision anything (:120-126).
  • Why it's built this way: ADR-066 records the local Service Bus emulator path as part of transport selection, ADR-016 records the MassTransit v8 pin that forces the custom-clients overload and the quota lowering, and ADR-118 records the transport-library policy behind that pin. Keeping all of it in one internal static type is what lets the production branch of AzureServiceBusTransport stay a one-liner, so a reader of the transport wiring sees the production path first and the development affordance as an explicit detour.

  • Where it's used: AzureServiceBusTransport.Configure calls IsEmulatorConnectionString and, on a match, ConfigureEmulatorHost(cfg, connectionString, settings.EmulatorAdminEndpoint) (MMCA.Common/Source/Hosting/MMCA.Common.Azure/Messaging/AzureServiceBusTransport.cs:41-45, the production cfg.Host(connectionString) in the else at :48). The admin endpoint is bound from MessageBus:EmulatorAdminEndpoint (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Messaging/MessageBusSettings.cs:56), which an Aspire AppHost sets from the emulator resource (MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs:304). The test tier applies the same quota lowering from its own fixture, ServiceBusEmulatorFixtureBase (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Azure/Fixtures/ServiceBusEmulatorFixtureBase.cs:74-76), which pins the emulator image (:61). MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/Messaging/ServiceBusEmulatorSupportTests.cs covers marker detection, the endpoint swap, the loud failure on a missing or non-HTTP admin endpoint, and the quota constant.

  • Caveats / not-in-source: whether a given AppHost run uses the emulator or RabbitMQ is a host and environment decision, so "which transport a developer is on right now" is Not determinable from source here; the source only settles that the emulator branch is reachable exactly when the resolved connection string carries the marker.


AwsSecretsManagerConfigurationSource

MMCA.Common.Aws · MMCA.Common.Aws.Secrets · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Secrets/AwsSecretsManagerConfigurationSource.cs:14 · Level 1 · class (internal sealed)

  • What it is: the IConfigurationSource that carries a client factory, a name prefix and an optional reload interval, and builds one AwsSecretsManagerConfigurationProvider from them (AwsSecretsManagerConfigurationSource.cs:14-28).
  • Depends on: AwsSecretsManagerConfigurationProvider; externals IAmazonSecretsManager, IConfigurationSource.
  • Concept: the standard source/provider split of Microsoft.Extensions.Configuration. [Rubric §14, Testability] assesses whether a dependency can be substituted: taking a Func<IAmazonSecretsManager> rather than a client lets a test hand in a fake, and gives each Build a fresh client that the provider owns and disposes.
  • Walkthrough: a primary constructor over clientFactory, namePrefix and reloadInterval (:14-17); NamePrefix (:20) and ReloadInterval (:23) are exposed internally so tests can assert what the extension configured; Build calls the factory and constructs the provider (:26-27).
  • Why it's built this way: the factory defers client construction to the moment configuration is built, after the extension has resolved credentials and endpoint.
  • Where it's used: added by AwsSecretsManagerConfigurationExtensions.AddAwsSecretsManagerConfiguration (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsSecretsManagerConfigurationExtensions.cs:127-130) only when SecretsManager:NamePrefix is set (:85-94), with a static key pair when one is configured (:97-98), and a ReloadIntervalMinutes that must parse as a positive whole number or the call throws (:103-115). Asserted in MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/AwsSecretsManagerConfigurationExtensionsTests.cs.

AzureNotificationHubNativePushSender

MMCA.Common.Azure · MMCA.Common.Azure.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Notifications/AzureNotificationHubNativePushSender.cs:15 · Level 1 · class (public sealed partial)

  • What it is: the Azure implementation of INativePushSender, sending both the FCM v1 and the APNs payload for every notification through an Azure Notification Hubs client, either to a set of users (resolved by tag) or to every installation (AzureNotificationHubNativePushSender.cs:15-45).

  • Depends on: Microsoft.Azure.NotificationHubs.INotificationHubClient and ILogger<T> as its two primary-constructor parameters (AzureNotificationHubNativePushSender.cs:15-17), NativePushPayloads for the payloads, NativePushUserTags for the tag expressions, and INativePushSender as the contract.

  • Concept introduced, one logical notification, two platform sends. [Rubric §10, Messaging & Integration] assesses whether a capability that varies by platform is normalized behind one application-facing call. A caller says "notify these users, with this title and body"; it does not know or care which of them are on iOS. This class turns that into two hub calls per audience chunk, one per platform, and the hub decides which installations each applies to. [Rubric §29, Resilience & Business Continuity] applies through the delivery posture named in the class doc: callers treat this channel as best effort, and the send handler wraps it in a non-fatal catch (:12-13), which is the side-effect contract of ADR-096. A push that fails does not fail the command that triggered it.

  • Walkthrough

    • SendToUsersAsync (AzureNotificationHubNativePushSender.cs:20-32) builds each payload once (:22-23), then loops the tag expressions produced by NativePushUserTags (:25), awaiting an FcmV1Notification send and an AppleNotification send per chunk (:27-28). For an audience of 45 users that is 3 chunks and 6 hub calls, and the payload serialization happened twice in total.
    • BroadcastAsync (AzureNotificationHubNativePushSender.cs:35-41) is the untagged form: the same two notification types, built inline, with no tag expression, so the hub fans out to every installation it holds (:37-38).
    • Both paths end with the same source-generated Information log carrying the title (:31, :40, declared at :43-44). The wording is precise: the notification was handed to the hub. Actual delivery to a device is asynchronous and outside this process.
    • Every await uses ConfigureAwait(false), which is the library policy of ADR-049.
  • Why it's built this way: sending the two payloads unconditionally rather than looking up each user's platform keeps the sender free of any device registry of its own. The hub already knows each installation's platform from the registration written by AzureNotificationHubDeviceRegistrar, so a payload that does not apply to a given installation is simply not delivered to it. That is the division of labour ADR-044 records: the hub is the device registry, and this codebase stores no push tokens. ADR-134 records why it lives in the vendor adapter assembly rather than the core.

  • Where it's used: registered only by AzureServiceCollectionExtensions.AddAzureNotificationHubsPush (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:101), and only when AzureNotificationHubSettings has Enabled: true, a connection string and a hub name (:90-96); the hub client is built from the connection string in the same call (:98-100). MMCA.ADC's Notification module makes that call (MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.API/DependencyInjection.cs:40), so the channel is switched on by configuration alone. The consuming application code is SendPushNotificationHandler (MMCA.Common/Source/Core/MMCA.Common.Application/Notifications/PushNotifications/UseCases/Send/SendPushNotificationHandler.cs:47, :142), where the best-effort catch lives. The AWS and GCP siblings are SnsNativePushSender and FcmNativePushSender.

  • Caveats / not-in-source: the only test reference is the registration assertion in MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/AzureServiceCollectionExtensionsTests.cs; there is no dedicated test of the send loop. Whether a given deployment has working platform credentials is a provisioning question, Not determinable from source.


S3ObjectUrl

MMCA.Common.Aws · MMCA.Common.Aws.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Storage/S3ObjectUrl.cs:9 · Level 1 · class (internal static)

  • What it is: the pure function that composes the absolute URL an S3 upload returns, from the settings, the client's resolved region and the object key (S3ObjectUrl.cs:9-69).
  • Depends on: S3FileStorageSettings; BCL Uri, UriBuilder.
  • Concept: the S3 API returns no object URL on PutObject, so the adapter derives one. [Rubric §14, Testability] assesses whether logic with many branches is isolated where it can be tested without I/O: this is a static function with no client, covered by MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Storage/S3ObjectUrlTests.cs.
  • Walkthrough: Compose (:29-63) escapes the key first (:34), then takes the first branch that applies. A PublicBaseUrl that is absolute wins: {PublicBaseUrl}/{key}, without repeating the bucket (:36-39). An absolute ServiceUrl gives {ServiceUrl}/{bucket}/{key} when ForcePathStyle is set (:45-48), otherwise a UriBuilder prepends the bucket to the host (:50-51). Otherwise AWS S3 in the resolved region, path-style or virtual-hosted (:60-62); with no region known it throws an InvalidOperationException naming the three settings that would fix it (:54-58). EscapeKey splits on /, applies Uri.EscapeDataString to each segment and rejoins, so separators survive and spaces or non-ASCII characters do not break the URL (:65-66). TrimEnd drops a trailing slash from a base (:68).
  • Why it's built this way: the doc orders the branches explicitly (:11-23) because each vendor addresses objects differently; a CDN or R2 public domain already points at the bucket, so repeating it would produce a broken link.
  • Where it's used: S3FileStorageService.UploadAsync composes the URL before uploading (MMCA.Common/Source/Hosting/MMCA.Common.Aws/Storage/S3FileStorageService.cs:46).

S3StorageClientFactory

MMCA.Common.Aws · MMCA.Common.Aws.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Storage/S3StorageClientFactory.cs:10 · Level 1 · class (internal static)

  • What it is: maps S3FileStorageSettings onto an AmazonS3Config and creates the IAmazonS3 client, with static credentials or the AWS default chain (S3StorageClientFactory.cs:10-54).
  • Depends on: S3FileStorageSettings, AwsClientEndpoint, AwsStaticCredentials; externals Amazon.S3 (AmazonS3Config, AmazonS3Client).
  • Concept: endpoint and credential selection shared with the other AWS adapters through AwsClientEndpoint and AwsStaticCredentials, so S3, Secrets Manager and SNS resolve regions and keys the same way. [Rubric §11, Security] assesses whether production uses an identity rather than a stored key: with no key pair configured the client falls back to the default credential chain (an IAM role in production).
  • Walkthrough: SectionPath = "FileStorage" names the section in configuration errors (:13). BuildConfig (:23-37) copies ForcePathStyle and both checksum modes (:27-32), then lets AwsClientEndpoint.Apply choose between a service URL with a signing region and a plain region (:34). Create (:45-53) resolves credentials (:48) and constructs AmazonS3Client with or without them (:50-52).
  • Why it's built this way: keeping the mapping in a static factory makes the config testable without a network call (MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Storage/S3StorageClientFactoryTests.cs). ADR-134 and ADR-045 cover the adapter and the storage contract.
  • Where it's used: AddS3FileStorage resolves the key pair against SectionPath at registration, so a half-configured pair fails there rather than on the first upload, then registers Create as the IAmazonS3 singleton with TryAddSingleton, so a client the host registered first wins (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:64-66).

AzureBlobFileStorageService

MMCA.Common.Azure · MMCA.Common.Azure.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Storage/AzureBlobFileStorageService.cs:20 · Level 4 · class (public sealed)

  • What it is: the Azure Blob Storage implementation of IFileStorageService. It uploads and deletes blobs inside the one container the host configured, and returns a Result rather than letting a storage or transport exception escape (AzureBlobFileStorageService.cs:10-20).
  • Depends on: Azure.Storage.Blobs.BlobContainerClient and ILogger<T> as its two primary-constructor parameters (AzureBlobFileStorageService.cs:20-22), IFileStorageService as the contract, FileUploadOptions for the extra headers, and Result / Error for its return shapes.
  • Concept introduced, taking the container as a dependency rather than creating it. [Rubric §11, Security] assesses whether an application holds only the permissions it needs, and [Rubric §17, DevOps] assesses whether resource provisioning lives with infrastructure rather than in application startup. The class doc is explicit (AzureBlobFileStorageService.cs:11-13): the container, and its public-access level, is provisioned by infrastructure and not created here. A CreateIfNotExists in application code would need container-management rights at runtime. [Rubric §13, Observability & Operability] also applies: failures are logged with the exception and translated into a domain-shaped failure whose message says nothing about Azure, so the operator sees the cause and the caller sees a stable error code.
  • Walkthrough
    • IsConfigured is a constant true (AzureBlobFileStorageService.cs:25). Its purpose is to differ from the false on NullFileStorageService.
    • UploadAsync(blobName, content, contentType, ct) (:29-30) forwards to the options overload with FileUploadOptions.None. Both overloads carry an RS0026 suppression (:28, :33): the pair is grandfathered on the interface, and the justification records that the type moved here from MMCA.Common.Infrastructure, so both overloads are unshipped in this package.
    • The real work is UploadAsync(blobName, content, contentType, options, ct) (:34-72). It null-guards options (:36), resolves a blob client (:40), and uploads with BlobUploadOptions carrying the content type plus options.ContentDisposition and options.CacheControl as HTTP headers (:41-52), returning the blob's URI on success (:54).
    • Two catches yield the same Error.Failure with code FileStorage.UploadFailed: RequestFailedException, the Azure SDK's service failure (:56-63), and any exception IsTransportFailure accepts (:64-71).
    • IsTransportFailure (:107-109) accepts HttpRequestException, IOException, or an OperationCanceledException the caller did not request (a timeout after the client's own retries, :101-105). The caller's own cancellation is excluded and keeps propagating.
    • DeleteAsync(blobName, ct) (:75-99) uses DeleteBlobIfExistsAsync (:79), so an unknown blob name is success, which is what the interface promises ("unknown names succeed (idempotent)", MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/Storage/IFileStorageService.cs:43). The same two catches yield FileStorage.DeleteFailed (:83-98).
  • Why it's built this way: ADR-045 records blob storage as the home for binary content the databases should not hold, and ADR-123 extends it to speaker and session assets. The registration is where the credential story lives (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:41-71): an absolute ServiceUri with DefaultAzureCredential is the managed-identity production path, ConnectionString is the local Azurite path, and ContainerName is required for either (:49-66). An incomplete section returns before registering anything, which leaves the null default in place, so a host can call AddAzureBlobFileStorage unconditionally.
  • Where it's used: registered by AddAzureBlobFileStorage (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:68), which MMCA.ADC's Identity and Conference service hosts call (MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:261, MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:367). Consumers depend on IFileStorageService only. The S3 and GCS siblings are S3FileStorageService and GcsFileStorageService. Covered by MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/Storage/AzureBlobFileStorageServiceTests.cs.
  • Caveats / not-in-source: whether the configured container is public-read or served through a signed URL is a provisioning decision, Not determinable from source.

AzureNotificationHubDeviceRegistrar

MMCA.Common.Azure · MMCA.Common.Azure.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Notifications/AzureNotificationHubDeviceRegistrar.cs:15 · Level 4 · class (public sealed)

  • What it is: the Azure implementation of IPushDeviceRegistrar. It maintains device installations in Azure Notification Hubs using the installation model (client-generated stable ids, full upsert semantics) and stamps each installation with its owner's user:{id} tag so sends can target users rather than raw device tokens (AzureNotificationHubDeviceRegistrar.cs:15-141).

  • Depends on: INotificationHubClient and ILogger<T> as its two primary-constructor parameters (AzureNotificationHubDeviceRegistrar.cs:15-17), NativePushUserTags for the owner tag, DeviceInstallationRequest as the input shape, and Result / Error.

  • Concept introduced, the hub as the only ownership store, and the existence oracle it must not become. [Rubric §11, Security] assesses whether an endpoint can be used to learn something the caller is not entitled to know, and [Rubric §9, API & Contract Design] assesses whether the result shape is honest about what happened. Installation ids are client-generated, so nothing stops a caller from sending someone else's, and the upsert is a full overwrite that would re-tag and re-point another user's device. The framework stores no device table of its own, so both checks run against the hub: the delete is a read-then-delete comparing the owner tag (AzureNotificationHubDeviceRegistrar.cs:80-92), and the upsert first asks ClaimedByAnotherDeviceAsync (:107-131). The installation id is per device, so a second user signing in on the same phone legitimately presents an id the previous user owns; when the request also carries the installation's current push channel the caller holds the device and the installation is handed over (re-tagged, so the previous owner's pushes stop). Without the matching channel the upsert answers with a conflict, which reveals nothing exploitable because ids are random client GUIDs, and the client recovers by minting a fresh id (:46-60). The delete path reports a mismatch as success, not as not-found; the interface remarks explain why (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/Notifications/IPushDeviceRegistrar.cs:45): answering differently for "no such installation" and "not yours" would turn the endpoint into an existence oracle for other users' installation ids.

  • Walkthrough

    • UpsertAsync(userId, request, ct) (AzureNotificationHubDeviceRegistrar.cs:20-73) starts with the platform switch: FCMV1 and APNS, compared after ToUpperInvariant, map to NotificationPlatform.FcmV1 and .Apns, and anything else yields null (:22-27).
    • An unmapped platform returns a validation failure with code PushDevice.UnsupportedPlatform, and the message quotes the two accepted constants from DeviceInstallationRequest (:28-34).
    • The Installation is assembled with the client-supplied id, the resolved platform, the push channel, and exactly one tag, the owner stamp from NativePushUserTags.UserTag (:36-42). Before writing, the upsert calls ClaimedByAnotherDeviceAsync; when it returns true the result is Error.Conflict with code PushDevice.InstallationConflict (:54-60). CreateOrUpdateInstallationAsync is a full upsert, so re-registering the same id replaces the record rather than accumulating duplicates (:62).
    • ClaimedByAnotherDeviceAsync (:113-131) is true only when all three hold: the installation exists (a MessagingEntityNotFoundException on the read means free, :120-123), it carries a user:-prefixed tag that is not the caller's own (:125-128), and its stored push channel differs from the request's, compared ordinally (:129-130).
    • The upsert's catch is narrow (MessagingException) and translates to PushDevice.UpsertFailed after logging the real exception (:65-72).
    • DeleteAsync(userId, installationId, ct) (:76-105) reads the installation, checks ownership, and deletes only on a match (:84-92). The comment at :80-83 settles the concurrency question: a concurrent re-registration of the same id between the read and the delete is the owner's own doing, so no lock is warranted.
    • Two catches follow. MessagingEntityNotFoundException returns success, because an unknown installation is already the desired state (:95-99); a general MessagingException logs and returns PushDevice.DeleteFailed (:100-104).
    • OwnedBy (:133-134) checks the tags contain the owner tag with StringComparer.Ordinal. DeleteFailed (:136-140) is factored out so the failure shape is written once.
  • Why it's built this way: ADR-044 puts the device registry in the hub rather than in the application database, which keeps push tokens (a credential-shaped secret) out of the application's storage entirely. The cost of that choice is the read-then-delete ownership check here, and the interface remarks accept it explicitly.

  • Where it's used: registered by AddAzureNotificationHubsPush alongside the sender (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:102). Called from the framework's DevicesController (MMCA.Common/Source/Presentation/MMCA.Common.API/Controllers/Notifications/DevicesController.cs:49, :73), which lets a client register a device without any application-specific endpoint. The AWS and GCP siblings are SnsPushDeviceRegistrar and FcmPushDeviceRegistrar. Covered by MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/Notifications/AzureNotificationHubDeviceRegistrarTests.cs.

  • Caveats / not-in-source: an installation registered before ownership tagging carries no user: tag, and the delete path treats it exactly like someone else's (success, no delete), as the comment at :87-88 says. On the upsert side such an installation is claimable by any caller, because it has no other user's owner tag. Nothing in this code cleans up such records.


AzureServiceBusTransport

MMCA.Common.Azure · MMCA.Common.Azure.Messaging · MMCA.Common/Source/Hosting/MMCA.Common.Azure/Messaging/AzureServiceBusTransport.cs:16 · Level 4 · class (internal sealed)

  • What it is: the IBrokerTransport that wires MassTransit onto Azure Service Bus when MessageBus:Provider is AzureServiceBus (AzureServiceBusTransport.cs:16-56).
  • Depends on: IBrokerTransport, BrokerTransportContext (settings plus resolved connection string), BrokerTransportPolicies, ServiceBusEmulatorSupport; external MassTransit (IBusRegistrationConfigurator.UsingAzureServiceBus).
  • Concept introduced, a pluggable broker transport per vendor. [Rubric §10, Messaging & Integration] assesses whether the broker is a replaceable detail behind the application's messaging: the core picks a transport by name, and this class is the Azure one, living in the vendor assembly beside AmazonSqsTransport. [Rubric §17, DevOps] applies through the emulator branch: a development stack runs the same transport as production (:37-40).
  • Walkthrough: ProviderName = "AzureServiceBus" (:19) is returned by Name (:22). Configure (:25-55) null-guards both arguments (:27-28), reads settings and connection string from the context (:30-31), and inside UsingAzureServiceBus (:33) does three things. With a non-blank connection string it either hands off to ServiceBusEmulatorSupport.ConfigureEmulatorHost when the emulator marker is present (:41-45) or calls cfg.Host(connectionString) (:48). It then applies the shared receive policies with delayed redelivery enabled (:52) and calls ConfigureEndpoints (:53).
  • Why it's built this way: ADR-066 records transport selection by provider name; ADR-087 the receive and poison-message policies applied through BrokerTransportPolicies; ADR-016 and ADR-118 the MassTransit v8 pin and library policy that shape the emulator path. Moving the vendor transport out of the core keeps the Azure SDK out of hosts that do not use it, which the architecture rules check by naming MassTransit.AzureServiceBusTransport among vendor namespaces (MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Rules/Layering/ArchitectureRules.CloudVendors.cs:35).
  • Where it's used: registered by AzureServiceCollectionExtensions.AddAzureServiceBusTransport (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:117-118), documented to be called before AddBrokerMessaging (:107-115). Every MMCA.ADC service host calls it (for example MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:339, MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:428, MMCA.ADC/Source/Services/MMCA.ADC.Engagement.Service/Program.cs:298, MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:238). Covered by MMCA.Common/Tests/Hosting/MMCA.Common.Azure.Tests/Messaging/AzureServiceBusTransportTests.cs.
  • Caveats / not-in-source: which provider each deployment selects is configuration, Not determinable from source.

S3FileStorageService

MMCA.Common.Aws · MMCA.Common.Aws.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Aws/Storage/S3FileStorageService.cs:23 · Level 4 · class (public sealed)

  • What it is: the S3-compatible implementation of IFileStorageService: AWS S3, Cloudflare R2, MinIO and the GCS XML API through one client, against the single bucket in FileStorage:Bucket, with the same contract as AzureBlobFileStorageService (S3FileStorageService.cs:11-23).
  • Depends on: IAmazonS3, IOptions<S3FileStorageSettings> and ILogger<T> as primary-constructor parameters (:23-26); S3FileStorageSettings, S3ObjectUrl, FileUploadOptions, Result / Error.
  • Concept: the same provider-neutral storage contract introduced at AzureBlobFileStorageService. [Rubric §29, Resilience & Business Continuity] assesses whether a write cannot leave inconsistent state behind: the object URL is composed before the upload, so a configuration that cannot name the object fails before any orphan is written (:44-46). [Rubric §32, Dependency & Supply-Chain] applies through the vendor-specific signing choice in SendsUnsignedPayload.
  • Walkthrough
    • _settings unwraps the options with a null guard (:28); IsConfigured is true (:31). The two UploadAsync overloads carry the same RS0026 suppressions as the Azure sibling (:34, :39).
    • UploadAsync(..., options, ct) (:40-95) composes the URL with the client's resolved region (:46), builds a PutObjectRequest with AutoCloseStream = false because the caller owns the stream (:48-57), sets DisablePayloadSigning when SendsUnsignedPayload is true (:59-62), and copies non-empty ContentDisposition and CacheControl headers (:64-72). Success returns the composed URI (:76-77); AmazonServiceException and transport failures both yield FileStorage.UploadFailed (:79-94).
    • DeleteAsync (:98-125) calls DeleteObjectAsync; the comment notes S3 delete is already idempotent (a missing key answers 204), which matches the contract (:102-106). Failures yield FileStorage.DeleteFailed (:109-124).
    • SendsUnsignedPayload (:137-139) is true only for an absolute HTTPS ServiceUrl. The doc gives the reason (:127-134): Cloudflare R2 does not implement streaming SigV4 chunked signing, TLS already protects the body, AWS S3 keeps the SDK default, and plain-HTTP endpoints (MinIO, LocalStack) must keep it because the SDK refuses an unsigned payload over HTTP.
    • IsTransportFailure (:147-149) is the same predicate as the Azure sibling: HttpRequestException, IOException, or a cancellation the caller did not request.
  • Why it's built this way: ADR-134 keeps the S3 SDK in a vendor assembly so the core stays cloud-neutral; ADR-045 and ADR-123 define the storage contract it honours. One S3-compatible adapter covering several vendors avoids a near-duplicate adapter per bucket provider.
  • Where it's used: registered by AddS3FileStorage (MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:67) when FileStorage:Bucket is set. Tested in MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Storage/S3FileStorageServiceTests.cs and MMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/AwsServiceCollectionExtensionsTests.cs.
  • Caveats / not-in-source: no host in MMCA.ADC/Source calls AddS3FileStorage, so which deployments use this adapter, and against which vendor, is Not determinable from source here.

CloudSqlAccessTokenSource

MMCA.Common.Gcp · MMCA.Common.Gcp.CloudSql · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/CloudSql/CloudSqlAccessTokenSource.cs:12 · Level 0 · class (internal sealed)

  • What it is: the default token source behind CloudSqlIamAuthenticationConfigurator: it returns an OAuth2 access token for the Cloud SQL login scope from Application Default Credentials (CloudSqlAccessTokenSource.cs:5-10).
  • Depends on: nothing first-party; Google GoogleCredential and ITokenAccess (Google.Apis.Auth.OAuth2).
  • Concept: cross-reference CloudSqlIamAuthenticationConfigurator, which introduces the GCP form of token-as-password database authentication (the AWS form is RdsIamAuthenticationConfigurator).
  • Walkthrough: Scope is https://www.googleapis.com/auth/sqlservice.login (CloudSqlAccessTokenSource.cs:15). _credential (CloudSqlAccessTokenSource.cs:17) caches the scoped credential. GetTokenAsync(cancellationToken) (CloudSqlAccessTokenSource.cs:22) reads the cached field, and when it is null loads GoogleCredential.GetApplicationDefaultAsync and calls CreateScoped(Scope) (CloudSqlAccessTokenSource.cs:27-32), then returns GetAccessTokenForRequestAsync(authUri: null, ...) (CloudSqlAccessTokenSource.cs:35).
  • Why it's built this way: the credential is loaded on the first request rather than at registration, so a host builds its container without Google credentials and only the first database connection needs them (CloudSqlAccessTokenSource.cs:7-9). The credential caches and refreshes its own token, so each call is an in-memory read until expiry nears (CloudSqlAccessTokenSource.cs:9-10). The lazy load is deliberately unlocked: a race between two first callers loads the credential twice and keeps the last, and a failed load is not cached, so the next call (after Npgsql's failure interval) tries again (CloudSqlAccessTokenSource.cs:24-26). See ADR-113.
  • Where it's used: the public constructor of CloudSqlIamAuthenticationConfigurator binds its GetTokenAsync as the token delegate (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/CloudSql/CloudSqlIamAuthenticationConfigurator.cs:34-35); referenced by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/CloudSql/CloudSqlIamAuthenticationTests.cs.

CloudSqlIamAuthenticationSettings

MMCA.Common.Gcp · MMCA.Common.Gcp.CloudSql · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/CloudSql/CloudSqlIamAuthenticationSettings.cs:10 · Level 0 · class (internal sealed)

  • What it is: the bound options for Cloud SQL IAM database authentication, section CloudSql:IamAuthentication (CloudSqlIamAuthenticationSettings.cs:13).
  • Depends on: nothing first-party.
  • Concept: cross-reference CloudSqlIamAuthenticationConfigurator, which introduces the GCP token-as-password flow.
  • Walkthrough: a single member, Enabled (CloudSqlIamAuthenticationSettings.cs:16), off when absent. Unlike the AWS sibling RdsIamAuthenticationSettings it carries no region or credential keys, because the token comes from Application Default Credentials (CloudSqlAccessTokenSource).
  • Why it's built this way: an explicit boolean gate lets a deployment switch PostgreSQL onto IAM tokens by configuration alone, and an absent or false value makes registration a no-op (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:95-98). See ADR-070, ADR-113 and ADR-134.
  • Where it's used: bound in AddCloudSqlIamAuthentication of GcpServiceCollectionExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:94); SectionPath is quoted in the TLS error of CloudSqlIamAuthenticationConfigurator (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/CloudSql/CloudSqlIamAuthenticationConfigurator.cs:106).

GcpKmsXmlDecryptor

MMCA.Common.Gcp · MMCA.Common.Gcp.DataProtection · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpKmsXmlDecryptor.cs:16 · Level 0 · class (internal sealed)

  • What it is: the ASP.NET Core DataProtection IXmlDecryptor that reverses GcpKmsXmlEncryptor, decrypting a stored key element with the Cloud KMS key that element names (GcpKmsXmlDecryptor.cs:10-15).
  • Depends on: GcpKmsXmlEncryptor for the element-name constants; Google KeyManagementServiceClient, DecryptRequest, ByteString; IServiceProvider.
  • Concept: cross-reference GcpKmsXmlEncryptor for the envelope format. The decryptor half of a DataProtection pair is activated by type: the encryptor records the decryptor's type in the key ring, and DataProtection later instantiates it through an IServiceProvider constructor (GcpKmsXmlDecryptor.cs:12-14).
  • Walkthrough: _client (GcpKmsXmlDecryptor.cs:18) is resolved in the IServiceProvider constructor with GetRequiredService<KeyManagementServiceClient>() (GcpKmsXmlDecryptor.cs:22-25). Decrypt(encryptedElement) (GcpKmsXmlDecryptor.cs:29) reads the kmsKeyName and value children (GcpKmsXmlDecryptor.cs:33-34) and throws InvalidOperationException naming both elements when either is blank (GcpKmsXmlDecryptor.cs:35-39). It then sends a synchronous DecryptRequest with the named key and the base64-decoded ciphertext (GcpKmsXmlDecryptor.cs:41-45) and parses the UTF-8 plaintext back into an XElement (GcpKmsXmlDecryptor.cs:47).
  • Why it's built this way: the key name is read from the element rather than from configuration, so a ring encrypted under one key keeps decrypting regardless of current settings. Taking the client from the container means it is the one AddGoogleCloudDataProtection registers or one the host registered first (GcpKmsXmlDecryptor.cs:14-15); see GcpDataProtectionBuilderExtensions.
  • Where it's used: named as the decryptor type in GcpKmsXmlEncryptor.Encrypt (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpKmsXmlEncryptor.cs:68); exercised by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/DataProtection/GcpKmsXmlEncryptionTests.cs.

GcpKmsXmlEncryptor

MMCA.Common.Gcp · MMCA.Common.Gcp.DataProtection · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpKmsXmlEncryptor.cs:25 · Level 0 · class (internal sealed)

  • What it is: the DataProtection IXmlEncryptor that encrypts each key element with a Cloud KMS symmetric key before it reaches the key ring, so the stored object holds only ciphertext (GcpKmsXmlEncryptor.cs:9-13).
  • Depends on: GcpKmsXmlDecryptor (named in the result); Google KeyManagementServiceClient, CryptoKeyName, EncryptRequest, ByteString; DataProtection EncryptedXmlInfo.
  • Concept introduced, key-ring encryption at rest with a cloud KMS. DataProtection lets a host plug an IXmlEncryptor between key generation and the repository; the encryptor returns the stored form plus the decryptor type that can reverse it. [Rubric §11, Security] assesses protection of secrets at rest; here reading a key needs cloudkms.cryptoKeyVersions.useToDecrypt on top of read access to the bucket (GcpKmsXmlEncryptor.cs:11-12), so a leaked bucket alone does not leak the keys.
  • Walkthrough: three element-name constants, encryptedKey, kmsKeyName and value (GcpKmsXmlEncryptor.cs:28,31,34). The internal constructor takes the client and a CryptoKeyName, null-checking both (GcpKmsXmlEncryptor.cs:41-45); KeyName exposes the key (GcpKmsXmlEncryptor.cs:48). Encrypt(plaintextElement) (GcpKmsXmlEncryptor.cs:51) serializes the element with SaveOptions.DisableFormatting to UTF-8 (GcpKmsXmlEncryptor.cs:55), sends a synchronous EncryptRequest (GcpKmsXmlEncryptor.cs:56-60), builds <encryptedKey> with an explanatory comment, the key name and the base64 ciphertext (GcpKmsXmlEncryptor.cs:62-66), and returns EncryptedXmlInfo(encrypted, typeof(GcpKmsXmlDecryptor)) (GcpKmsXmlEncryptor.cs:68).
  • Why it's built this way: the element is sent to KMS whole because a key element is about a kilobyte, far under the 64 KiB plaintext limit (GcpKmsXmlEncryptor.cs:12-13), so no local envelope key is needed. The ciphertext records the key version, so rotating the KMS key needs no code change and old keys keep decrypting while their version is enabled (GcpKmsXmlEncryptor.cs:21-22).
  • Where it's used: constructed by ProtectKeysWithGoogleCloudKms in GcpDataProtectionBuilderExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpDataProtectionBuilderExtensions.cs:59); its constants are read by GcpKmsXmlDecryptor; exercised by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/DataProtection/GcpKmsXmlEncryptionTests.cs and MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/GoogleCloudDataProtectionExtensionsTests.cs.

GcsObjectLocation

MMCA.Common.Gcp · MMCA.Common.Gcp.DataProtection · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcsObjectLocation.cs:11 · Level 0 · record (internal sealed)

  • What it is: the Cloud Storage object that holds the DataProtection key ring, a positional record of Bucket and ObjectName parsed from the gs://bucket/object form of DataProtection:GcsObjectUri (GcsObjectLocation.cs:3-11).
  • Depends on: nothing first-party.
  • Concept: a small parse-once value that fails loudly on a malformed setting, the fail-fast configuration contract of ADR-070.
  • Walkthrough: Scheme is gs:// (GcsObjectLocation.cs:13). Parse(value, key) (GcsObjectLocation.cs:23) matches the scheme case-insensitively (GcsObjectLocation.cs:27), splits the remainder at the first / (GcsObjectLocation.cs:29-30), and accepts it only when the bucket is non-empty, the object name is non-empty and the value does not end in / (GcsObjectLocation.cs:31-33); anything else throws InvalidOperationException naming the configuration key and the bad value (GcsObjectLocation.cs:37-38). ToString() reassembles gs://bucket/object (GcsObjectLocation.cs:42).
  • Why it's built this way: parsed by hand rather than through Uri, which lower-cases and re-escapes an unknown scheme's authority and path in ways a bucket or object name must not be (GcsObjectLocation.cs:5-7). A trailing / is rejected because it names a folder, not an object (GcsObjectLocation.cs:16-17).
  • Where it's used: parsed in GoogleCloudDataProtectionExtensions before anything is registered (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudDataProtectionExtensions.cs:90), passed through PersistKeysToGoogleCloudStorage (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpDataProtectionBuilderExtensions.cs:34) into GcsXmlRepository; exercised by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/DataProtection/GcsObjectLocationTests.cs.

GcsXmlRepository

MMCA.Common.Gcp · MMCA.Common.Gcp.DataProtection · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcsXmlRepository.cs:31 · Level 1 · class (internal sealed)

  • What it is: a DataProtection IXmlRepository that keeps the whole key ring as one Cloud Storage object, a <repository> document whose children are the key elements, the same shape the Azure Blob repository uses (GcsXmlRepository.cs:12-15).
  • Depends on: GcsObjectLocation; Google StorageClient, GoogleApiException, UploadObjectOptions, DownloadObjectOptions; System.Xml.Linq.
  • Concept introduced, optimistic concurrency on an object store. Writes are read-modify-write under an object generation precondition, the Cloud Storage counterpart of an ETag: the upload carries IfGenerationMatch set to the generation it read (0 when the object did not exist, which lets the write only create it), so two replicas adding a key at once cannot overwrite each other (GcsXmlRepository.cs:17-22). [Rubric §29, Resilience & Business Continuity] assesses whether scale-out keeps sessions valid; one shared, lost-update-free ring is what lets every replica unprotect the others' cookies and antiforgery tokens. [Rubric §12, Performance & Scalability] also applies: concurrent writers converge by retry rather than by a lock.
  • Walkthrough: MaxAttempts is 10 (GcsXmlRepository.cs:34); the root element is repository (GcsXmlRepository.cs:36); WriterSettings emit UTF-8 without a byte order mark (GcsXmlRepository.cs:39). The constructor takes the client, the location and a retryDelay backoff unit (GcsXmlRepository.cs:48-53). GetAllElements() returns the root's children from a fresh read (GcsXmlRepository.cs:59-60). StoreElement(element, friendlyName) (GcsXmlRepository.cs:63) loops up to MaxAttempts (GcsXmlRepository.cs:67): it reads the document and its generation (GcsXmlRepository.cs:69), appends a copy of the element so the caller's element is never re-parented into a discarded document (GcsXmlRepository.cs:71-72), serializes to a stream (GcsXmlRepository.cs:74-80), and uploads with IfGenerationMatch = generation (GcsXmlRepository.cs:84-88). A 412 PreconditionFailed means another writer won, so it sleeps retryDelay * attempt (linear backoff) and re-reads the winner's ring (GcsXmlRepository.cs:90-97). After the last attempt it throws naming the location and the attempt count (GcsXmlRepository.cs:101-102). The private Read() (GcsXmlRepository.cs:105) fetches metadata, returns an empty ring at generation 0 on 404 (GcsXmlRepository.cs:114-119), downloads pinned to the metadata's generation (GcsXmlRepository.cs:125-129), and loops when that generation vanished between the two calls (GcsXmlRepository.cs:131-134), returning the parsed document and generation (GcsXmlRepository.cs:138).
  • Why it's built this way: pinning the download to the generation the metadata reported guarantees the generation a write is conditioned on is the one whose content it extended (GcsXmlRepository.cs:22-23). IXmlRepository is synchronous and the storage client has synchronous methods, so nothing blocks on a task; DataProtection reads the ring rarely and writes only when it creates a key (GcsXmlRepository.cs:25-28). See ADR-069; the Azure and AWS counterparts are AzureDataProtectionExtensions and AwsDataProtectionExtensions.
  • Where it's used: constructed by PersistKeysToGoogleCloudStorage in GcpDataProtectionBuilderExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpDataProtectionBuilderExtensions.cs:38); exercised by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/DataProtection/GcsXmlRepositoryTests.cs and MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/GoogleCloudDataProtectionExtensionsTests.cs.
  • Caveats / not-in-source: the friendlyName parameter is not used by this implementation (GcsXmlRepository.cs:63-102).

CloudSqlIamAuthenticationConfigurator

MMCA.Common.Gcp · MMCA.Common.Gcp.CloudSql · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/CloudSql/CloudSqlIamAuthenticationConfigurator.cs:23 · Level 2 · class (internal sealed)

  • What it is: an INpgsqlDataSourceConfigurator that puts every PostgreSQL data source on Cloud SQL IAM database authentication: the password becomes an OAuth2 access token for the sqlservice.login scope, handed to Npgsql through a periodic password provider (CloudSqlIamAuthenticationConfigurator.cs:8-13).
  • Depends on: INpgsqlDataSourceConfigurator, ConnectionStringSettings, DataSourcesSettings, CloudSqlIamAuthenticationSettings, CloudSqlAccessTokenSource; Npgsql NpgsqlDataSourceBuilder.UsePeriodicPasswordProvider.
  • Concept: token-as-password database auth, introduced on the AWS sibling RdsIamAuthenticationConfigurator; the GCP difference is that the token is fetched from Google credentials rather than signed locally per host. [Rubric §11, Security] assesses credential handling: no database password exists, and because the token is a bearer credential sent as the password, a connection string that does not require TLS is rejected (CloudSqlIamAuthenticationConfigurator.cs:11-13). [Rubric §14, Testability] assesses whether external dependencies can be substituted; an internal constructor takes the token source as a delegate.
  • Walkthrough: TokenRefreshInterval is one minute (CloudSqlIamAuthenticationConfigurator.cs:26) and FailureRetryInterval 30 seconds (CloudSqlIamAuthenticationConfigurator.cs:29). The public constructor chains to new CloudSqlAccessTokenSource().GetTokenAsync (CloudSqlIamAuthenticationConfigurator.cs:34-35); the internal one accepts any Func<CancellationToken, Task<string>> for tests (CloudSqlIamAuthenticationConfigurator.cs:41-42). Configure(builder) (CloudSqlIamAuthenticationConfigurator.cs:46) checks TLS on the builder's connection string, naming its host (CloudSqlIamAuthenticationConfigurator.cs:50-52), then installs UsePeriodicPasswordProvider(GetPasswordAsync, TokenRefreshInterval, FailureRetryInterval) (CloudSqlIamAuthenticationConfigurator.cs:54). EnsureConfiguredConnectionStringsAreEncrypted(configuration) (CloudSqlIamAuthenticationConfigurator.cs:65) collects the top-level ConnectionStrings:PostgreSQLConnectionString and every DataSources:{name}:PostgreSQLConnectionString (CloudSqlIamAuthenticationConfigurator.cs:69-74) and checks each non-blank one parsed through NpgsqlDataSourceBuilder, the same parser Configure sees (CloudSqlIamAuthenticationConfigurator.cs:76-81). GetPasswordAsync (CloudSqlIamAuthenticationConfigurator.cs:90) honours cancellation and awaits the token source (CloudSqlIamAuthenticationConfigurator.cs:93-95); the token does not depend on the connection settings. EnsureEncrypted (CloudSqlIamAuthenticationConfigurator.cs:98) accepts Require, VerifyCA or VerifyFull (CloudSqlIamAuthenticationConfigurator.cs:100) and otherwise throws a message quoting CloudSql:IamAuthentication:Enabled, the source and the fix (CloudSqlIamAuthenticationConfigurator.cs:105-108).
  • Why it's built this way: the token lives an hour, but the refresh interval is one minute because the bound is the credential's cache: Google's credential serves its cached token until fewer than 225 seconds remain and blocks for a new one only under 60, while Npgsql reuses what it was handed for the whole interval; a one-minute interval keeps every held token valid, and each refresh is an in-memory read (CloudSqlIamAuthenticationConfigurator.cs:15-21). Checking configured connection strings at registration makes a cleartext misconfiguration fail at startup rather than on the first query (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:100-102), while Configure still catches one supplied later (ADR-070). See ADR-061, ADR-113 and ADR-134.
  • Where it's used: registered by AddCloudSqlIamAuthentication in GcpServiceCollectionExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:102,104); exercised by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/CloudSql/CloudSqlIamAuthenticationTests.cs.
  • Caveats / not-in-source: SQL Server, SQLite and Cosmos sources are untouched; the registration doc comment records that Cloud SQL for SQL Server has no IAM database authentication and that behind the Cloud SQL Auth Proxy its --auto-iam-authn flag is used instead (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:78-83). The required roles (roles/cloudsql.instanceUser, roles/cloudsql.client, same comment, line 79) are not checked by code.

GcpDataProtectionBuilderExtensions

MMCA.Common.Gcp · MMCA.Common.Gcp.DataProtection · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpDataProtectionBuilderExtensions.cs:18 · Level 2 · class (internal static)

  • What it is: the two IDataProtectionBuilder extensions that wire the GCP key ring: PersistKeysToGoogleCloudStorage (repository) and ProtectKeysWithGoogleCloudKms (encryptor).
  • Depends on: GcsXmlRepository, GcsObjectLocation, GcpKmsXmlEncryptor, GcsStorageClientFactory, GcsFileStorageSettings; externals KeyManagementOptions, ConfigureOptions<T>, Google StorageClient, KeyManagementServiceClient, CryptoKeyName.
  • Concept: options-based plumbing of a DataProtection repository and encryptor. Each method registers an IConfigureOptions<KeyManagementOptions> whose factory resolves services lazily, so the client is chosen at options time rather than at registration. [Rubric §1, SOLID] assesses dependency inversion; here both collaborators are taken from the container when the host provided them.
  • Walkthrough: RetryDelay is 50 ms, the backoff unit handed to the repository (GcpDataProtectionBuilderExtensions.cs:21). PersistKeysToGoogleCloudStorage(location) (GcpDataProtectionBuilderExtensions.cs:34) sets options.XmlRepository to a new GcsXmlRepository over the host's registered StorageClient, or else GcsStorageClientFactory.Create(new GcsFileStorageSettings()) (GcpDataProtectionBuilderExtensions.cs:36-41). ProtectKeysWithGoogleCloudKms(keyName) (GcpDataProtectionBuilderExtensions.cs:54) TryAddSingletons a default KeyManagementServiceClient.Create() (GcpDataProtectionBuilderExtensions.cs:56) and sets options.XmlEncryptor to a GcpKmsXmlEncryptor over the container's client (GcpDataProtectionBuilderExtensions.cs:57-59).
  • Why it's built this way: the fallback storage client is deliberately NOT added to the container, because a default registered here would win the TryAdd in a later AddGoogleCloudStorage and silently ignore its FileStorage:GcsServiceUrl (GcpDataProtectionBuilderExtensions.cs:24-29). The KMS client, by contrast, must be in the container, because DataProtection activates GcpKmsXmlDecryptor per key and that type resolves the client there (GcpDataProtectionBuilderExtensions.cs:47-49). See ADR-069.
  • Where it's used: called only from GoogleCloudDataProtectionExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudDataProtectionExtensions.cs:109,113), which calls the KMS step only when a Cloud KMS key name was configured and parsed (GoogleCloudDataProtectionExtensions.cs:92-98,111-114).

FcmInstallation

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmInstallation.cs:13 · Level 0 · record (internal sealed)

  • What it is: the in-memory shape of one Firestore installation document: InstallationId, UserId, Platform, PushChannel (FcmInstallation.cs:13-17), where PushChannel is the FCM registration token.
  • Depends on: nothing first-party beyond the UserIdentifierType alias (see the identifier-type aliases in the primer); System.Globalization.
  • Concept: cross-reference FirestoreInstallationRegistry, which explains why FCM needs a side registry at all (an FCM token carries no owner). It is the GCP twin of SnsInstallation, minus the SNS endpoint and subscription ARNs: FCM addresses the token directly and topic membership lives in FCM, not in the row.
  • Walkthrough: a positional record with no nullable members (FcmInstallation.cs:13-17). UserKey(userId) (FcmInstallation.cs:22-23) renders a user id as an invariant-culture string with string.Create(CultureInfo.InvariantCulture, ...); that string is what the UserId field stores and what queries filter on, so every writer and reader goes through this one method.
  • Where it's used: materialized by FirestoreInstallationRegistry.FromSnapshot (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FirestoreInstallationRegistry.cs:109-113), built by FcmPushDeviceRegistrar (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmPushDeviceRegistrar.cs:112), grouped by token in FcmNativePushSender (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:52-54), and the element type of every IFcmInstallationRegistry member.

FcmNativePushSettings

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSettings.cs:12 · Level 0 · class (internal sealed partial)

  • What it is: the bound options for Firebase Cloud Messaging push, section NativePush:Fcm (FcmNativePushSettings.cs:15), gated by the shared sibling key NativePush:Enabled (FcmNativePushSettings.cs:18).
  • Depends on: nothing first-party; BCL Regex source generation (GeneratedRegex).
  • Concept: cross-reference GcpServiceCollectionExtensions (AddFirebaseCloudMessagingPush) for the gate-plus-completeness registration pattern this class feeds; the AWS sibling is SnsNativePushSettings. [Rubric §15, Best Practices & Code Quality] assesses whether configuration errors surface early and legibly; here a malformed topic name is rejected with a message that names the key and the allowed alphabet.
  • Walkthrough: ProjectId (FcmNativePushSettings.cs:24) is the Google Cloud project that owns both the Firebase app and the Firestore database (FcmNativePushSettings.cs:20-23); InstallationsCollection (FcmNativePushSettings.cs:27) is the Firestore collection, one document per installation; BroadcastTopic (FcmNativePushSettings.cs:33) is the FCM topic every registered token subscribes to, a bare name or the /topics/ form (FcmNativePushSettings.cs:29-32). IsComplete (FcmNativePushSettings.cs:36-39) requires all three to be non-blank. EnsureValidTopic() (FcmNativePushSettings.cs:46-53) throws InvalidOperationException naming NativePush:Fcm:BroadcastTopic when the topic fails TopicPattern, a source-generated regex ^(/topics/)?[a-zA-Z0-9\-_.~%]+$ with a 1000 ms match timeout (FcmNativePushSettings.cs:55-56).
  • Why it's built this way: unlike SNS there is no credential or endpoint setting; the registration doc comment states credentials come from Application Default Credentials only, so there is no key-file setting (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:123-125). The topic check runs at registration so a typo fails startup rather than every device registration and broadcast (FcmNativePushSettings.cs:41-44), the posture of ADR-070; an incomplete section, by contrast, is a registration no-op.
  • Where it's used: AddFirebaseCloudMessagingPush reads the gate and section, calls EnsureValidTopic, and binds the options (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:146-158); FcmPushClientFactory takes it directly; FirestoreInstallationRegistry, FcmNativePushSender and FcmPushDeviceRegistrar read it through IOptions<FcmNativePushSettings>. Covered by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/AddFirebaseCloudMessagingPushTests.cs.

FcmSendOutcome

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmSendOutcome.cs:12 · Level 0 · record (internal sealed)

  • What it is: the result of one message in an FCM batch send: IsSuccess, an optional FirebaseAdmin MessagingErrorCode, and the optional exception (FcmSendOutcome.cs:12).
  • Depends on: nothing first-party; FirebaseAdmin MessagingErrorCode.
  • Concept: a vendor-neutral-to-this-package outcome type, so the sender can be tested against a fake IFcmMessagingClient without constructing FirebaseAdmin's own batch response. [Rubric §29, Resilience & Business Continuity] assesses whether a failure is classified before it is acted on; here only a failure that indicts the token prunes data.
  • Walkthrough: Success (FcmSendOutcome.cs:15) is the shared success instance. IsStaleToken (FcmSendOutcome.cs:25-28) is true for MessagingErrorCode.Unregistered, or for InvalidArgument only when the exception message contains "registration token" (ordinal, case-insensitive).
  • Why it's built this way: the doc comment records the asymmetry (FcmSendOutcome.cs:17-24): FCM answers a malformed token with InvalidArgument and a message naming the registration token, but any other InvalidArgument is about the message itself, would fail every token alike, and must never prune them. See ADR-044.
  • Where it's used: produced by FirebaseMessagingClient (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FirebaseMessagingClient.cs:31-33) and by the test doubles FakeFcmMessagingClient and RecordingFcmMessagingClient; consumed by FcmNativePushSender (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:74-85).
  • Caveats / not-in-source: the stale-token classification depends on FCM's English error text; whether that wording is stable is not determinable from source.

GoogleSecretManagerConfigurationProvider

MMCA.Common.Gcp · MMCA.Common.Gcp.Secrets · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Secrets/GoogleSecretManagerConfigurationProvider.cs:33 · Level 0 · class (internal sealed)

  • What it is: the ConfigurationProvider that loads every Google Secret Manager secret under one id prefix into IConfiguration, mapping adc-prod-ConnectionStrings--Default to ConnectionStrings:Default (GoogleSecretManagerConfigurationProvider.cs:12-13), with an optional timed reload.
  • Depends on: nothing first-party; Google SecretManagerServiceClient, Grpc.Core.RpcException, Microsoft.Extensions.Configuration.ConfigurationProvider.
  • Concept: the GCP sibling of AwsSecretsManagerConfigurationProvider and of the Key Vault provider used by AzureKeyVaultConfigurationExtensions: one naming convention across clouds, so a secret set moves by renaming its prefix. [Rubric §11, Security] assesses whether secrets stay out of files and images and whether access is least-privilege; here values come from the vault at startup and each failure message names the exact IAM permission and role to grant. [Rubric §29, Resilience & Business Continuity] applies to the reload: a failed refresh keeps the last good values.
  • Walkthrough: constants KeyDelimiter = "--" and LatestVersion = "latest" (GoogleSecretManagerConfigurationProvider.cs:35-36); state is the client, project, prefix, optional interval, a disposal CancellationTokenSource, a Lock timer gate, the timer and a disposed flag (GoogleSecretManagerConfigurationProvider.cs:38-45). The internal constructor null-checks client, project and prefix (GoogleSecretManagerConfigurationProvider.cs:52-62). KeyFor(secretId, namePrefix) (GoogleSecretManagerConfigurationProvider.cs:74-82) returns null unless the id starts with the prefix ordinally and is longer than it, then strips the prefix and replaces -- with the configuration separator. Load() (GoogleSecretManagerConfigurationProvider.cs:85-99) sets Data = Read() and, with an interval, creates a one-shot Timer once under the gate (GoogleSecretManagerConfigurationProvider.cs:89-98). ReloadOnce (GoogleSecretManagerConfigurationProvider.cs:119-147) re-reads, swaps Data and calls OnReload() only when HasSameData is false (GoogleSecretManagerConfigurationProvider.cs:125-129), records any non-cancellation exception in LastReloadFailure (GoogleSecretManagerConfigurationProvider.cs:65,131-136), and re-arms the timer in finally unless disposed (GoogleSecretManagerConfigurationProvider.cs:139-145). Read() (GoogleSecretManagerConfigurationProvider.cs:149-186) lists secrets with the filter name:"{prefix}" (GoogleSecretManagerConfigurationProvider.cs:156), keeps only ids KeyFor accepts (GoogleSecretManagerConfigurationProvider.cs:164), wraps a listing RpcException in an InvalidOperationException naming secretmanager.secrets.list (GoogleSecretManagerConfigurationProvider.cs:170-175), then reads each value into a case-insensitive dictionary. ReadLatest (GoogleSecretManagerConfigurationProvider.cs:188-218) gets the latest version, returns null on NotFound (GoogleSecretManagerConfigurationProvider.cs:197-201) or a non-Enabled state (GoogleSecretManagerConfigurationProvider.cs:203-206), and otherwise accesses the concrete version name the state check saw, decoding the payload as UTF-8 (GoogleSecretManagerConfigurationProvider.cs:210). Dispose() (GoogleSecretManagerConfigurationProvider.cs:102-117) stops the timer under the gate and cancels in-flight calls.
  • Why it's built this way: the service's name filter is a case-insensitive substring match, so the ordinal prefix re-check keeps another environment's secrets out (GoogleSecretManagerConfigurationProvider.cs:17-19). Accessing the concrete version rather than the latest alias again means a rotation landing between the two calls cannot return a value whose state was never checked (GoogleSecretManagerConfigurationProvider.cs:208-209). Every call is synchronous because ConfigurationProvider.Load is synchronous and the host needs the values before the services that read them are registered; the one-shot re-armed timer means two loads never overlap (GoogleSecretManagerConfigurationProvider.cs:24-30). A throttled call or revoked grant during a reload must not take down a host that already has its values (GoogleSecretManagerConfigurationProvider.cs:133-134). See ADR-061 and ADR-134.
  • Where it's used: built only by GoogleSecretManagerConfigurationSource (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Secrets/GoogleSecretManagerConfigurationSource.cs:31-32); covered by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Secrets/GoogleSecretManagerConfigurationProviderTests.cs.
  • Caveats / not-in-source: LastReloadFailure is internal and nothing in the defining file surfaces it to logs or health checks; whether any caller reads it outside tests is not shown in this brief.

GoogleSecretManagerConfigurationSource

MMCA.Common.Gcp · MMCA.Common.Gcp.Secrets · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Secrets/GoogleSecretManagerConfigurationSource.cs:15 · Level 1 · class (internal sealed)

  • What it is: the IConfigurationSource that carries a client factory, the project id, the name prefix and the reload interval, and builds a GoogleSecretManagerConfigurationProvider.
  • Depends on: GoogleSecretManagerConfigurationProvider; Google SecretManagerServiceClient, IConfigurationSource.
  • Concept: the standard configuration source/provider split; cross-reference AwsSecretsManagerConfigurationSource for the identical AWS shape.
  • Walkthrough: primary-constructor parameters clientFactory, projectId, namePrefix, reloadInterval (GoogleSecretManagerConfigurationSource.cs:15-19), with ProjectId, NamePrefix and ReloadInterval exposed as internal read-only properties (GoogleSecretManagerConfigurationSource.cs:22,25,28). Build (GoogleSecretManagerConfigurationSource.cs:31-32) invokes the factory and constructs the provider.
  • Why it's built this way: taking a Func<SecretManagerServiceClient> rather than a client defers client creation to Build, and the internal properties let tests assert what the extension method configured without a live client.
  • Where it's used: added by AddGoogleSecretManagerConfiguration with SecretManagerServiceClient.Create as the factory (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleSecretManagerConfigurationExtensions.cs:118-119), see GoogleSecretManagerConfigurationExtensions; asserted by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/GoogleSecretManagerConfigurationExtensionsTests.cs.

IFcmInstallationRegistry

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/IFcmInstallationRegistry.cs:9 · Level 1 · interface (internal)

  • What it is: the adapter-internal contract over the installation registry: read one, claim one under the ownership rule, query by users, and two conditional deletes.
  • Depends on: FcmInstallation.
  • Concept introduced, an internal port for a vendor store. The registry sits behind an interface even though it is internal so that FcmNativePushSender and FcmPushDeviceRegistrar can be unit-tested against FakeFcmInstallationRegistry while the Firestore implementation is covered by the emulator tier (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Emulator.Tests/FcmPushFirestoreEmulatorTests.cs). [Rubric §14, Testability] assesses whether infrastructure can be substituted in tests; here the substitution point is the adapter's own boundary, not the core IPushDeviceRegistrar.
  • Walkthrough: GetAsync (IFcmInstallationRegistry.cs:15) returns null for an unknown id. TryClaimAsync (IFcmInstallationRegistry.cs:29) writes atomically and returns (Claimed, PreviousPushChannel); it is refused only when the id exists with another owner AND a different push channel (IFcmInstallationRegistry.cs:17-22), and the previous channel lets the caller move the topic subscription. QueryByUsersAsync (IFcmInstallationRegistry.cs:35) takes distinct stored user keys. DeleteOwnedAsync (IFcmInstallationRegistry.cs:42) deletes only while the given user still owns the id; DeleteIfPushChannelAsync (IFcmInstallationRegistry.cs:52) deletes only while the id still holds the given token, so a stale-token cleanup never removes a registration refreshed onto a new token (IFcmInstallationRegistry.cs:44-47).
  • Where it's used: implemented by FirestoreInstallationRegistry, registered with TryAddSingleton (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:161); consumed by FcmNativePushSender and FcmPushDeviceRegistrar.

IFcmMessagingClient

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/IFcmMessagingClient.cs:11 · Level 1 · interface (internal)

  • What it is: the adapter-internal wrapper over the four FirebaseAdmin messaging calls this package uses: batch send, single send, topic subscribe and topic unsubscribe.
  • Depends on: FcmSendOutcome; FirebaseAdmin Message.
  • Concept: cross-reference IFcmInstallationRegistry for the internal-port rationale. FirebaseAdmin's FirebaseMessaging is a sealed singleton bound to a process-global app, so the interface is what lets tests substitute FakeFcmMessagingClient or RecordingFcmMessagingClient.
  • Walkthrough: MaxMessagesPerSend = 500 (IFcmMessagingClient.cs:17), FirebaseAdmin's own limit on SendEachAsync (IFcmMessagingClient.cs:13-16). SendEachAsync (IFcmMessagingClient.cs:23) returns one outcome per message, in order. SendAsync (IFcmMessagingClient.cs:29) throws on failure. SubscribeToTopicAsync and UnsubscribeFromTopicAsync (IFcmMessagingClient.cs:36,43) take one token and return FCM's refusal reason, or null on success, rather than throwing for a per-token refusal.
  • Why it's built this way: returning the refusal reason as a value lets the registrar treat a refused subscribe as a failed upsert and a refused unsubscribe as informational, while a transport failure still throws. See ADR-044.
  • Where it's used: implemented by FirebaseMessagingClient, registered as a TryAddSingleton factory (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:160); consumed by FcmNativePushSender and FcmPushDeviceRegistrar.

FirebaseMessagingClient

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FirebaseMessagingClient.cs:17 · Level 2 · class (internal sealed)

  • What it is: the production IFcmMessagingClient, which lazily creates its own uniquely named FirebaseApp over Application Default Credentials and delegates to FirebaseMessaging.
  • Depends on: IFcmMessagingClient, FcmSendOutcome; FirebaseAdmin FirebaseApp, FirebaseMessaging, TopicManagementResponse; Google GoogleCredential.
  • Concept: lazy, serialized initialization of a process-global resource. [Rubric §2, Design Patterns] assesses whether patterns solve a real problem; here a double-checked lazy init under a SemaphoreSlim guards a name that FirebaseAdmin lets be created only once.
  • Walkthrough: fields _gate (a SemaphoreSlim(1, 1)), _app, _messaging (FirebaseMessagingClient.cs:19-21); AppName is MMCA.Common.Gcp.Fcm.{Guid:N}, unique per instance (FirebaseMessagingClient.cs:24). SendEachAsync (FirebaseMessagingClient.cs:27-34) maps each batch response to FcmSendOutcome.Success or an outcome carrying MessagingErrorCode and the exception (FirebaseMessagingClient.cs:31-33). SendAsync (FirebaseMessagingClient.cs:37-41) delegates directly. The topic methods (FirebaseMessagingClient.cs:44-55) send a one-token list and pass the response through FirstError (FirebaseMessagingClient.cs:66-74), which returns null when FailureCount == 0, else the first error's Reason or "unknown-error". GetMessagingAsync (FirebaseMessagingClient.cs:76-100) returns the cached instance, otherwise waits on the gate, re-checks, fetches GoogleCredential.GetApplicationDefaultAsync, and calls FirebaseApp.Create(..., AppName) and FirebaseMessaging.GetMessaging (FirebaseMessagingClient.cs:89-91). Dispose deletes the app and disposes the gate (FirebaseMessagingClient.cs:58-64).
  • Why it's built this way: the gate exists because two first callers must not race to create the same app name (FirebaseMessagingClient.cs:83). The registration doc comment records why each instance owns a uniquely named app: FirebaseAdmin's app registry is process-global, so the registration creates its own app on first use and deletes it with the service provider, leaving a host's own default app unaffected (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:132-134). Deferring credential lookup to first use keeps registration free of a network call.
  • Where it's used: created by FcmPushClientFactory.CreateMessaging (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmPushClientFactory.cs:41-46), see FcmPushClientFactory; exercised in MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/AddFirebaseCloudMessagingPushTests.cs.
  • Caveats / not-in-source: the fast path reads _messaging without the gate (FirebaseMessagingClient.cs:78-81); a call racing Dispose is not guarded, and whether that can happen in practice depends on container shutdown order, which is not determinable from this file.

FirestoreInstallationRegistry

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FirestoreInstallationRegistry.cs:16 · Level 2 · class (internal sealed)

  • What it is: the Firestore implementation of IFcmInstallationRegistry: one document per installation id in the configured collection, with the ownership rules enforced inside Firestore transactions.
  • Depends on: IFcmInstallationRegistry, FcmInstallation, FcmNativePushSettings; Google FirestoreDb, CollectionReference, DocumentSnapshot, FieldValue.
  • Concept: ownership rules as a read-decide-write transaction. The AWS sibling SnsInstallationRegistry expresses the same rule as a DynamoDB condition expression; Firestore has no conditional put, so the rule runs inside RunTransactionAsync, which retries on contention so two writers cannot both win. [Rubric §8, Data Architecture] assesses consistency under concurrency; [Rubric §11, Security] applies because a user cannot take over another device's installation id.
  • Walkthrough: MaxUsersPerQuery = 30 (FirestoreInstallationRegistry.cs:23), Firestore's limit on one WhereIn filter; UserId equality is served by the automatic single-field index, so no composite index is needed (FirestoreInstallationRegistry.cs:19-22). Field names UserId, Platform, PushChannel, UpdatedAt (FirestoreInstallationRegistry.cs:25-28); Collection resolves from the options on each access (FirestoreInstallationRegistry.cs:30). GetAsync (FirestoreInstallationRegistry.cs:33-37) reads one snapshot. TryClaimAsync (FirestoreInstallationRegistry.cs:40-66) reads the document inside the transaction, returns (false, existing.PushChannel) when it exists with another owner and another channel (FirestoreInstallationRegistry.cs:49-54), else sets all four fields with a server timestamp (FirestoreInstallationRegistry.cs:56-62) and returns (true, previous channel) (FirestoreInstallationRegistry.cs:63). QueryByUsersAsync (FirestoreInstallationRegistry.cs:69-87) chunks keys by 30 and runs one WhereIn query per chunk (FirestoreInstallationRegistry.cs:74-76). DeleteOwnedAsync and DeleteIfPushChannelAsync (FirestoreInstallationRegistry.cs:90-95) both go through DeleteWhenAsync (FirestoreInstallationRegistry.cs:116-133), a transaction that deletes only when the document exists and the condition still holds (FirestoreInstallationRegistry.cs:123-129). FromSnapshot (FirestoreInstallationRegistry.cs:97-114) returns null for a missing document or one lacking a non-empty UserId or PushChannel, and defaults a missing Platform to empty (FirestoreInstallationRegistry.cs:112).
  • Why it's built this way: an FCM token carries no owner, so the registry is the only place ownership can live (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:110-115). The conditional deletes make cleanup safe against interleaving: a document that changed hands or tokens since it was read is no longer this delete's to make (FirestoreInstallationRegistry.cs:125). See ADR-044.
  • Where it's used: registered as the singleton IFcmInstallationRegistry (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:161); its MaxUsersPerQuery constant is also read by FcmNativePushSender (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:39); covered against the emulator by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Emulator.Tests/FcmPushFirestoreEmulatorTests.cs.
  • Caveats / not-in-source: the Firestore database (native mode, the project's default database) is provisioned outside the application (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:128-129); nothing here creates or validates the collection.

FcmPushClientFactory

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmPushClientFactory.cs:11 · Level 3 · class (internal static)

  • What it is: builds the FirestoreDb and the FirebaseMessagingClient the FCM push adapter needs, from one FcmNativePushSettings.
  • Depends on: FcmNativePushSettings, FirebaseMessagingClient; Google FirestoreDbBuilder, EmulatorDetection.
  • Concept: the same factory shape as SnsPushClientFactory and GcsStorageClientFactory: pure functions from settings to clients, so registration code stays declarative and tests can inspect the builder.
  • Walkthrough: BuildFirestoreBuilder(settings) (FcmPushClientFactory.cs:22-31) returns a builder for the configured project with EmulatorDetection = EmulatorDetection.None (FcmPushClientFactory.cs:29). CreateFirestore (FcmPushClientFactory.cs:36) builds it. CreateMessaging (FcmPushClientFactory.cs:41-46) returns new FirebaseMessagingClient(settings.ProjectId!).
  • Why it's built this way: ignoring FIRESTORE_EMULATOR_HOST makes the endpoint a matter of configuration alone, the same posture as the Cloud Storage adapter; an emulator run registers its own FirestoreDb first (FcmPushClientFactory.cs:13-19).
  • Where it's used: the TryAddSingleton factories in AddFirebaseCloudMessagingPush (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:159-160), so a host-registered FirestoreDb or IFcmMessagingClient wins; asserted by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/AddFirebaseCloudMessagingPushTests.cs.

FcmPushDeviceRegistrar

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmPushDeviceRegistrar.cs:26 · Level 4 · class (internal sealed partial)

  • What it is: the FCM implementation of IPushDeviceRegistrar: it claims the installation in the Firestore registry, subscribes the token to the broadcast topic, and on delete reverses both.
  • Depends on: IPushDeviceRegistrar, DeviceInstallationRequest, Result, Error, IFcmInstallationRegistry, IFcmMessagingClient, FcmInstallation, FcmNativePushSettings; FirebaseAdmin FirebaseException, Grpc.Core.RpcException, LoggerMessage source generation.
  • Concept: cross-reference FirestoreInstallationRegistry for the ownership rule and SnsPushDeviceRegistrar for the AWS twin. Errors come back as Result failures, not exceptions. [Rubric §29, Resilience & Business Continuity] assesses ordering of side effects under partial failure; here every step is ordered so a client retry finishes the job rather than leaving a subscribed token with no document.
  • Walkthrough: Topic reads BroadcastTopic from options (FcmPushDeviceRegistrar.cs:32). UpsertAsync (FcmPushDeviceRegistrar.cs:35-65) upper-cases the platform (FcmPushDeviceRegistrar.cs:39); APNS gets a PushDevice.UnsupportedPlatform validation failure explaining that iOS clients must register an FCM token (FcmPushDeviceRegistrar.cs:40-46), and anything other than FCMV1 gets the same code (FcmPushDeviceRegistrar.cs:48-54); RpcException or FirebaseException maps to PushDevice.UpsertFailed (FcmPushDeviceRegistrar.cs:60-64,104-108). RegisterAsync (FcmPushDeviceRegistrar.cs:110-142) builds the installation with the normalized FcmV1Platform (FcmPushDeviceRegistrar.cs:112), calls TryClaimAsync (FcmPushDeviceRegistrar.cs:116) and returns PushDevice.InstallationConflict when refused (FcmPushDeviceRegistrar.cs:117-123), then subscribes the token on every upsert (FcmPushDeviceRegistrar.cs:127), failing the upsert on a refusal (FcmPushDeviceRegistrar.cs:128-132), and unsubscribes the previous token when the installation moved (FcmPushDeviceRegistrar.cs:134-139). UnsubscribeReplacedAsync (FcmPushDeviceRegistrar.cs:145-160) is best-effort and logs a FirebaseException as a warning. DeleteAsync (FcmPushDeviceRegistrar.cs:68-101) returns success for an unknown or foreign id (FcmPushDeviceRegistrar.cs:73-79), unsubscribes the token (FcmPushDeviceRegistrar.cs:85), then deletes conditionally on the owner (FcmPushDeviceRegistrar.cs:91); failures map to PushDevice.DeleteFailed (FcmPushDeviceRegistrar.cs:94-100). Three source-generated log methods follow (FcmPushDeviceRegistrar.cs:162-169).
  • Why it's built this way: the ownership decision comes before any subscription so another device's installation stays exactly as it is (FcmPushDeviceRegistrar.cs:114-115). Subscribing on every upsert is safe because FCM treats a repeat as a no-op, and it lets a retry after a failed subscription finish the job (FcmPushDeviceRegistrar.cs:125-126). Delete runs subscription first, document last, so a part-way failure leaves the document for the retry, and the owner-conditional delete spares a document handed over in the meantime (FcmPushDeviceRegistrar.cs:81-84). A stale subscription after a token move is waste, not a correctness problem, hence best-effort (FcmPushDeviceRegistrar.cs:157). Only FCM tokens are accepted: FCM reaches iOS when the app registers its FCM token and the message carries the APNs block (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:115-117). See ADR-044.
  • Where it's used: registered transient as IPushDeviceRegistrar by AddFirebaseCloudMessagingPush (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:163), replacing NullPushDeviceRegistrar; covered by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/FcmPushDeviceRegistrarTests.cs.
  • Caveats / not-in-source: UnsubscribeReplacedAsync catches only FirebaseException (FcmPushDeviceRegistrar.cs:155); any other exception from that call propagates to UpsertAsync, where an RpcException would still map to UpsertFailed even though the claim and subscription already succeeded.

FcmPushMessage

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmPushMessage.cs:16 · Level 6 · class (internal static)

  • What it is: builds the FirebaseAdmin Message for one registration token or for the broadcast topic, carrying both a cross-platform notification and an APNs block.
  • Depends on: nothing first-party; FirebaseAdmin Message, Notification, ApnsConfig, Aps, ApsAlert.
  • Concept: one message serves Android and iOS. [Rubric §2, Design Patterns] assesses whether shared construction is centralized; both send paths go through one private Build, so a targeted send and a broadcast cannot drift in payload shape. Unlike SnsPushMessage, it does not reuse the core payload builders, because FirebaseAdmin takes a typed Message rather than raw JSON.
  • Walkthrough: ApsKey = "aps" (FcmPushMessage.cs:19). ForToken (FcmPushMessage.cs:27-41) builds and sets message.Token inside a CS0618 suppression (FcmPushMessage.cs:37-39). ForTopic (FcmPushMessage.cs:49-54) builds and sets Topic. Build (FcmPushMessage.cs:56-76) null-checks title and body, sets Notification (FcmPushMessage.cs:64), copies metadata into Data only when non-empty (FcmPushMessage.cs:65), and sets an ApnsConfig with an ApsAlert plus the metadata as CustomData minus any aps key (FcmPushMessage.cs:66-74).
  • Why it's built this way: the obsolete Message.Token is kept deliberately: the adapter registers FCM registration tokens, the HTTP v1 API still accepts a token target, and the topic APIs the broadcast depends on take only registration tokens; the comment names the exit, moving registration and send to Firebase installation ids together once the topic APIs accept them (FcmPushMessage.cs:31-36). Filtering aps out of CustomData keeps metadata from clobbering the alert block (FcmPushMessage.cs:18). See ADR-044 and ADR-134.
  • Where it's used: FcmNativePushSender for targeted sends and the broadcast (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:59,95); asserted in MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/FcmNativePushSenderTests.cs.

FcmNativePushSender

MMCA.Common.Gcp · MMCA.Common.Gcp.Notifications · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:27 · Level 7 · class (internal sealed partial)

  • What it is: the FCM implementation of INativePushSender: per-user sends batch one message per distinct token, and a broadcast is one message to the FCM topic.
  • Depends on: INativePushSender, IFcmInstallationRegistry, IFcmMessagingClient, FcmInstallation, FcmPushMessage, FcmSendOutcome, FirestoreInstallationRegistry (for its MaxUsersPerQuery constant), FcmNativePushSettings; FirebaseAdmin FirebaseException, Grpc.Core.RpcException, LoggerMessage source generation.
  • Concept: cross-reference SnsNativePushSender for the AWS twin. [Rubric §29, Resilience & Business Continuity] assesses partial-failure behavior: a lookup failure skips only that batch of users, a failed send call skips only that chunk, and a failed token skips only that token. [Rubric §12, Performance & Scalability] applies too: sends go out in batches of up to 500 rather than one HTTP call per device.
  • Walkthrough: SendToUsersAsync (FcmNativePushSender.cs:34-90) converts ids with FcmInstallation.UserKey, de-duplicates, and chunks by FirestoreInstallationRegistry.MaxUsersPerQuery (FcmNativePushSender.cs:39), logging and skipping a batch whose query throws RpcException (FcmNativePushSender.cs:43-48). It groups installations by token so two ids holding one token are one device (FcmNativePushSender.cs:51-54), then sends chunks of IFcmMessagingClient.MaxMessagesPerSend (FcmNativePushSender.cs:57-63), logging and continuing on FirebaseException (FcmNativePushSender.cs:65-69). Per outcome it counts a success, prunes on IsStaleToken, or logs the error code (FcmNativePushSender.cs:71-86), and finally logs the delivered count (FcmNativePushSender.cs:89). BroadcastAsync (FcmNativePushSender.cs:93-98) sends FcmPushMessage.ForTopic to BroadcastTopic. PruneAsync (FcmNativePushSender.cs:100-114) deletes every installation in the group with the token-conditional DeleteIfPushChannelAsync, logging an RpcException. Seven source-generated log methods follow (FcmNativePushSender.cs:116-135).
  • Why it's built this way: a broadcast through a topic costs one FCM call regardless of device count, while user-targeted sends need the registry. The token-conditional prune never removes an installation refreshed onto a new token in the meantime (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/IFcmInstallationRegistry.cs:44-47). See ADR-044.
  • Where it's used: registered transient as INativePushSender by AddFirebaseCloudMessagingPush (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:162), replacing NullNativePushSender; covered by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/FcmNativePushSenderTests.cs.
  • Caveats / not-in-source: BroadcastAsync has no try/catch, so a FirebaseAdmin failure propagates to the caller (FcmNativePushSender.cs:93-98); how callers handle it is outside this file. The batching reads a constant from the concrete FirestoreInstallationRegistry rather than from IFcmInstallationRegistry (FcmNativePushSender.cs:39).

GcsFileStorageSettings

MMCA.Common.Gcp · MMCA.Common.Gcp.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Storage/GcsFileStorageSettings.cs:12 · Level 0 · class (public sealed)

  • What it is: the options class bound from the FileStorage section for the Google Cloud Storage adapter: the bucket, an optional public URL base, an optional endpoint override and an optional quota project (GcsFileStorageSettings.cs:12-45).
  • Depends on: nothing first-party; BCL Uri only.
  • Concept: one shared FileStorage section, vendor-prefixed keys. SectionName is "FileStorage" (:15), the same section AzureBlobFileStorageSettings and S3FileStorageSettings bind, but every key here carries a Gcs prefix, so a section written for another cloud never accidentally enables this adapter. [Rubric §33, Developer Experience] assesses whether configuration is discoverable and hard to misuse: every property is init-only and documents its own default and failure mode in its XML comment.
  • Walkthrough:
    • GcsBucket (:18): required; the comment states the bucket is provisioned by infrastructure, not created here (:17).
    • GcsPublicBaseUrl (:26): an optional CDN or load balancer base; when set, a returned URL is {GcsPublicBaseUrl}/{object} without the bucket (:20-25).
    • GcsServiceUrl (:36): an optional JSON API root such as the fake-gcs-server emulator. A plain-HTTP endpoint is treated as an emulator and called without credentials, so no bearer token travels in clear; an HTTPS endpoint keeps Application Default Credentials (:28-35).
    • GcsProjectId (:44): the optional x-goog-user-project quota project, needed only with user-account credentials such as gcloud auth application-default login (:38-43).
    • Both URI properties state "Only an absolute URL counts" (:24, :34); the consumers enforce that with IsAbsoluteUri patterns (see GcsObjectUrl and GcsStorageClientFactory).
  • Why it's built this way: ADR-045 records managed file storage, and ADR-134 the vendor-adapter split. There are no key or secret properties at all: authentication is Application Default Credentials only.
  • Where it's used: bound and gated by GcpServiceCollectionExtensions.AddGoogleCloudStorage (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:52-59), read by GcsStorageClientFactory, GcsObjectUrl, GcsFileStorageService and GcpDataProtectionBuilderExtensions. Covered by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsStorageClientFactoryTests.cs and GcsObjectUrlTests.cs.

IGoogleCloudTelemetryTokenSource

MMCA.Common.Gcp · MMCA.Common.Gcp.Telemetry · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Telemetry/IGoogleCloudTelemetryTokenSource.cs:8 · Level 0 · interface (internal)

  • What it is: a one-method internal abstraction that returns a current OAuth2 access token for the Google Cloud telemetry exporter: Task<string> GetTokenAsync(CancellationToken) (IGoogleCloudTelemetryTokenSource.cs:13).
  • Depends on: nothing first-party.
  • Concept: a test boundary around the credential. [Rubric §14, Testability] assesses whether external dependencies can be substituted in tests: the production implementation calls Google's credential APIs, and this interface lets the test project substitute FakeTelemetryTokenSource (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Telemetry/FakeTelemetryTokenSource.cs) without any Google dependency.
  • Walkthrough: the single member's cancellation token cancels both the credential load and the token request (:11).
  • Why it's built this way: registered with TryAddSingleton (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudTelemetryExporterExtensions.cs:129), so a test registers its fake first and the default does not replace it.
  • Where it's used: implemented by ApplicationDefaultTelemetryTokenSource, consumed by GoogleCloudTelemetryAccessToken, registered by GoogleCloudTelemetryExporterExtensions.

ApplicationDefaultTelemetryTokenSource

MMCA.Common.Gcp · MMCA.Common.Gcp.Telemetry · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Telemetry/ApplicationDefaultTelemetryTokenSource.cs:11 · Level 1 · class (internal sealed)

  • What it is: the production IGoogleCloudTelemetryTokenSource: it loads Google Application Default Credentials once, scopes them, and asks them for an access token on each call (ApplicationDefaultTelemetryTokenSource.cs:11-38).
  • Depends on: IGoogleCloudTelemetryTokenSource; externals GoogleCredential and ITokenAccess from Google.Apis.Auth.
  • Concept: lazy, retry-friendly credential load. [Rubric §11, Security] assesses least privilege: the requested scope is the broad cloud-platform (:19), and the comment records why: the narrower trace.append does not cover the logs this exporter also writes, so the effective bound is the identity's IAM roles (roles/telemetry.writer, roles/serviceusage.serviceUsageConsumer) rather than the scope (:13-18).
  • Walkthrough: Scope constant (:19); a nullable _credential cache (:21). GetTokenAsync (:24-37) loads GoogleCredential.GetApplicationDefaultAsync and CreateScoped(Scope) only when the cache is empty (:29-34), then returns GetAccessTokenForRequestAsync(authUri: null, ...) (:36). The comment states a race between two first callers loads twice and keeps the last (harmless), and a failed load is not cached, so the next refresh retries (:26-27).
  • Why it's built this way: nothing touches Google at registration; the credential is first loaded by the hosted refresher at host start (see GoogleCloudTelemetryAccessToken), which keeps a host without credentials startable. Telemetry rationale: ADR-041.
  • Where it's used: registered as the default token source by GoogleCloudTelemetryExporterExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudTelemetryExporterExtensions.cs:129).
  • Caveats / not-in-source: whether GetAccessTokenForRequestAsync serves a cached token or fetches a new one per call is decided inside Google.Apis.Auth, not in this file.

AzureDataProtectionExtensions

MMCA.Common.Azure · MMCA.Common.Azure · MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureDataProtectionExtensions.cs:20 · Level 1 · class (static, extension<TBuilder>)

  • What it is: AddAzureDataProtection(), which persists the ASP.NET Core DataProtection key ring to one Azure Blob and optionally encrypts it at rest with an Azure Key Vault key, so every replica of a scaled-out host shares one key ring (AzureDataProtectionExtensions.cs:26-29).
  • Depends on: HostIntegrationMarker and HostIntegrationNames; externals IHostApplicationBuilder, DefaultAzureCredential, AddDataProtection, and the Azure PersistKeysToAzureBlobStorage / ProtectKeysWithAzureKeyVault extensions.
  • Concept: the configuration-gated host integration with a startup receipt, taught at AwsDataProtectionExtensions. One difference: this method registers its receipt BEFORE the gate (:57), so the marker is present even when DataProtection:BlobStorageUri is blank. [Rubric §29, Resilience & Business Continuity] assesses whether scale-out keeps sessions valid: a shared key ring is what lets cookies and antiforgery tokens minted by one replica decrypt on another.
  • Walkthrough:
    • Keys (:32-42): DataProtection:BlobStorageUri (the gate), DataProtection:ApplicationName (defaults to the host application name), DataProtection:KeyVaultKeyUri (optional).
    • Gate 1 (:59-67): blank URI returns the builder untouched, so developer machines, test hosts and the Helpdesk seed keep the in-memory default.
    • Persistence (:69-77): one DefaultAzureCredential shared by both sinks so they share a token cache (:72-73), then SetApplicationName(...).PersistKeysToAzureBlobStorage(new Uri(blobStorageUri), credential).
    • Gate 2 (:86-90): ProtectKeysWithAzureKeyVault only when DataProtection:KeyVaultKeyUri is set.
  • Why it's built this way: the comment at :79-85 records the load-bearing decision: gate 2 is deliberately separate from gate 1 because the Key Vault Crypto User role is granted out of band and can lag the deployment; coupling them would turn an optional hardening gap into a total authentication outage. Identity requirements (Storage Blob Data Contributor, plus Key Vault Crypto User when gate 2 is on) are stated at :46-49. Recorded in ADR-069 and ADR-134; the GCP sibling is GoogleCloudDataProtectionExtensions.
  • Where it's used: MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:127 and MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:72. The tripwire message naming this call is at MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Integrations/HostIntegrationTripwire.cs:76.

AzureKeyVaultConfigurationExtensions

MMCA.Common.Azure · MMCA.Common.Azure · MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureKeyVaultConfigurationExtensions.cs:23 · Level 1 · class (static, extension<TBuilder>)

  • What it is: AddAzureKeyVaultConfiguration(), which adds an Azure Key Vault configuration source so every vault secret reads through IConfiguration and overrides the sources added before it (AzureKeyVaultConfigurationExtensions.cs:29-31).
  • Depends on: HostIntegrationMarker, HostIntegrationNames; externals AzureKeyVaultConfigurationOptions, AddAzureKeyVault, DefaultAzureCredential.
  • Concept: secrets as a configuration source, the Azure original of the convention AwsSecretsManagerConfigurationProvider and GoogleSecretManagerConfigurationExtensions mirror. [Rubric §11, Security] assesses whether secrets live outside code and reach the process through an identity-scoped channel: authentication is DefaultAzureCredential (managed identity when deployed), needing Key Vault Secrets User on the vault (:52-56).
  • Walkthrough:
    • Keys (:35-41): KeyVault:Uri (the gate) and optional KeyVault:ReloadIntervalMinutes; unset means the vault is read once at startup.
    • Naming (:45-49): a double dash maps onto the configuration separator, so ConnectionStrings--Default arrives as ConnectionStrings:Default.
    • Receipt then gate (:84-94): the marker is registered first, then a blank URI returns.
    • Reload interval (:102-113): parsed with InvariantCulture; a non-positive or non-integer value throws InvalidOperationException (:108-109) rather than silently falling back to "never reload", per the comment at :98-101.
    • Source (:115): builder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential(), options); an invalid URI surfaces as UriFormatException (documented at :74-76).
  • Why it's built this way: deliberately not part of AddServiceDefaults(), because service defaults run in every host including developer machines and tests (:59-63). Because ConfigurationManager loads each source as it is added, the vault is read synchronously at this call, so it belongs early in Program.cs (:66-70). Recorded in ADR-061 and ADR-134.
  • Where it's used: every ADC host: MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/Program.cs:68, MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:63, and the Identity, Conference, Engagement and Notification service Program.cs files (for example MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:112).

AzureMonitorExporterExtensions

MMCA.Common.Azure · MMCA.Common.Azure · MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureMonitorExporterExtensions.cs:12 · Level 1 · class (static, extension<TBuilder>)

  • What it is: AddAzureMonitorExporter(), which exports logs, metrics and traces to Azure Monitor through the Azure Monitor OpenTelemetry distro when APPLICATIONINSIGHTS_CONNECTION_STRING is set (AzureMonitorExporterExtensions.cs:24-28).
  • Depends on: HostIntegrationMarker, HostIntegrationNames; externals AddOpenTelemetry().UseAzureMonitor().
  • Concept: an ordering precondition enforced in code. [Rubric §13, Observability & Operability] assesses whether telemetry is exported reliably and filtered deliberately: the outbox-poll and probe filter processors registered by service defaults must run before this exporter's batch processor, otherwise the spans they un-record are exported anyway (:30-33).
  • Walkthrough: ConnectionStringKey is the public constant "APPLICATIONINSIGHTS_CONNECTION_STRING" (:18). The method first scans the service collection for a non-keyed HostIntegrationMarker named ServiceDefaultsTelemetry and throws InvalidOperationException when it is missing (:44-52), regardless of the gate, so a misordered host fails on a developer machine (:38-40). It then registers its own receipt (:56), returns when the connection string is blank (:58-61), and otherwise calls UseAzureMonitor() (:63).
  • Why it's built this way: the gate keeps local runs free of an Azure dependency; the receipt lets the startup tripwire fail a host that has the connection string but not the call (:54-55). Both this exporter and the vendor-neutral OTLP exporter can be active at once, each shipping its own copy (:32-33). Recorded in ADR-041 and ADR-134; the GCP sibling is GoogleCloudTelemetryExporterExtensions.
  • Where it's used: every ADC host, directly after service defaults (for example MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/Program.cs:75, MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:70, MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:122).

GcsObjectUrl

MMCA.Common.Gcp · MMCA.Common.Gcp.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Storage/GcsObjectUrl.cs:9 · Level 1 · class (internal static)

  • What it is: the pure function that composes the absolute URL an upload returns for one object (GcsObjectUrl.cs:28).
  • Depends on: GcsFileStorageSettings; BCL Uri.
  • Concept: the same split as S3ObjectUrl: URL composition lives outside the service so it is unit-testable with no client. [Rubric §14, Testability] applies directly, covered by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsObjectUrlTests.cs.
  • Walkthrough: PublicEndpoint is https://storage.googleapis.com/ (:12). Compose (:28-42) null-guards both arguments, escapes the name, then picks in order (:15-21): an absolute GcsPublicBaseUrl yields {base}/{object} with no bucket (:35-38); otherwise an absolute GcsServiceUrl or the public endpoint yields {endpoint}/{bucket}/{object} (:40-41). EscapeName splits on /, applies Uri.EscapeDataString to each segment and rejoins (:44-45), so spaces and non-ASCII names still produce a valid URL while keeping the separators. TrimEnd strips a trailing slash from the base (:47).
  • Why it's built this way: a CDN or load balancer domain already points at the bucket, so repeating the bucket would break the URL (:17-18).
  • Where it's used: GcsFileStorageService.UploadAsync (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Storage/GcsFileStorageService.cs:48).

GcsStorageClientFactory

MMCA.Common.Gcp · MMCA.Common.Gcp.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Storage/GcsStorageClientFactory.cs:11 · Level 1 · class (internal static)

  • What it is: maps GcsFileStorageSettings onto Google's StorageClientBuilder and builds the StorageClient (GcsStorageClientFactory.cs:23, :46).
  • Depends on: GcsFileStorageSettings; externals StorageClientBuilder, StorageClient (Google.Cloud.Storage.V1).
  • Concept: the builder/build split, the same shape as S3StorageClientFactory. BuildBuilder returns the unbuilt builder so tests assert the mapping without network access (MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsStorageClientFactoryTests.cs). [Rubric §11, Security] applies to the emulator rule: credentials are dropped only for a plain-HTTP endpoint, so a bearer token never crosses an unencrypted connection.
  • Walkthrough: BuildBuilder (:23-41) null-guards the settings and starts from a default builder (Google endpoint, Application Default Credentials). An absolute GcsServiceUrl sets BaseUri to {GcsServiceUrl}/storage/v1/ (:31) and sets UnauthenticatedAccess when the scheme is http (:32). A non-blank GcsProjectId sets QuotaProject (:35-38). Create is BuildBuilder(settings).Build() (:46).
  • Why it's built this way: the doc comment states STORAGE_EMULATOR_HOST is ignored (the builder default), so the endpoint is decided by configuration alone (:18-19).
  • Where it's used: GcpServiceCollectionExtensions.AddGoogleCloudStorage (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:61) and GcpDataProtectionBuilderExtensions.

GoogleCloudDataProtectionExtensions

MMCA.Common.Gcp · MMCA.Common.Gcp · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudDataProtectionExtensions.cs:22 · Level 1 · class (static, extension<TBuilder>)

  • What it is: AddGoogleCloudDataProtection(), which persists the DataProtection key ring to one Google Cloud Storage object and optionally encrypts each key at rest with a Cloud KMS key (GoogleCloudDataProtectionExtensions.cs:31-33).
  • Depends on: GcsObjectLocation, GcpDataProtectionBuilderExtensions (PersistKeysToGoogleCloudStorage, ProtectKeysWithGoogleCloudKms), HostIntegrationMarker, HostIntegrationNames; externals CryptoKeyName (Google.Cloud.Kms.V1), AddDataProtection.
  • Concept: the gated host integration taught at AwsDataProtectionExtensions, with validation before registration. [Rubric §29, Resilience & Business Continuity]: the object is one document rewritten under a generation precondition, so two replicas creating a key at once cannot overwrite each other; the loser re-reads and retries (:51-53).
  • Walkthrough:
    • Keys (:24-25, :37-47): DataProtection:GcsObjectUri (the gate, gs://bucket/path/keys.xml), DataProtection:GcpKmsKeyName (optional full crypto key name), DataProtection:ApplicationName (optional).
    • Gate (:78-87): blank returns with no receipt, so an object URI that appears later still trips the startup check (:80-83).
    • Validation (:89-98): GcsObjectLocation.Parse and CryptoKeyName.TryParse both fail with the offending key named before anything is registered.
    • Registration (:102-114): receipt, application name, PersistKeysToGoogleCloudStorage(location), then ProtectKeysWithGoogleCloudKms only when a key name parsed.
  • Why it's built this way: encryption is a separate optional step "for the reason the Azure sibling gives" (:63-64, see AzureDataProtectionExtensions). A host keeps exactly one key ring: setting this object together with DataProtection:BlobStorageUri or DataProtection:SsmParameterPrefix fails the startup tripwire (:66-68). IAM needs (storage.objects.get/create/delete, roles/cloudkms.cryptoKeyEncrypterDecrypter) are stated at :57-61. Recorded in ADR-069 and ADR-134.
  • Where it's used: no call site in MMCA.ADC/Source or elsewhere in MMCA.Common/Source; the tripwire message naming it is at MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Integrations/HostIntegrationTripwire.cs:96.
  • Caveats / not-in-source: the StorageClient reuse described at :53-56 (a host-registered client, else one built over Application Default Credentials) lives in GcpDataProtectionBuilderExtensions, not in this file.

GoogleCloudTelemetryAccessToken

MMCA.Common.Gcp · MMCA.Common.Gcp.Telemetry · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Telemetry/GoogleCloudTelemetryAccessToken.cs:28 · Level 1 · class (internal sealed partial)

  • What it is: an IHostedService that holds the current Google access token for the telemetry exporter and refreshes it in the background (GoogleCloudTelemetryAccessToken.cs:28-30).
  • Depends on: IGoogleCloudTelemetryTokenSource; externals IHostedService, ILogger, OpenTelemetry SuppressInstrumentationScope.
  • Concept: a background-refreshed credential so the HTTP send path never awaits a token. [Rubric §29, Resilience & Business Continuity] assesses whether a dependency failure degrades rather than takes down the host: a failed refresh keeps the last good token and is logged, never thrown, because "telemetry export is not worth failing the host over" (:65-67). [Rubric §13, Observability & Operability]: the refresh runs inside SuppressInstrumentationScope.Begin() (:75), so the credential's own metadata-server and token-endpoint calls are not traced and exported once a minute as noise (:73-74).
  • Walkthrough:
    • Intervals (:33, :36): RefreshInterval 1 minute, FailureRetryInterval 30 seconds.
    • State (:38-43): a stopping CancellationTokenSource, a volatile string? _current exposed as Current, and the loop task.
    • StartAsync (:46-50): one awaited refresh, then the loop is started with that result.
    • RefreshAsync (:71-90): reads the token; rethrows only a requested cancellation (:81-84), otherwise logs LogRefreshFailed and returns false (:85-89).
    • RefreshLoopAsync (:92-106): delays by the success or failure interval, refreshes, repeats until stopped.
    • StopAsync / Dispose (:53-60, :63): cancel, await the loop with the caller's token, dispose the token source.
    • Log (:108-109): a source-generated Warning stating traces and logs are not exported until a refresh succeeds.
  • Why it's built this way: the export path reads a held string instead of awaiting a credential, which also serves the synchronous Send override in GoogleCloudTelemetryAuthHandler. Telemetry rationale: ADR-041.
  • Where it's used: registered as a singleton and as a hosted service by GoogleCloudTelemetryExporterExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudTelemetryExporterExtensions.cs:130-131), read by GoogleCloudTelemetryAuthHandler. Exercised by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Telemetry/GoogleCloudTelemetryAuthHandlerTests.cs.
  • Caveats / not-in-source: StartAsync awaits the first refresh, so how long host start waits on an unreachable credential endpoint depends on Google.Apis.Auth's own timeouts, which are not visible here.

GoogleCloudTelemetryAuthHandler

MMCA.Common.Gcp · MMCA.Common.Gcp.Telemetry · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Telemetry/GoogleCloudTelemetryAuthHandler.cs:14 · Level 2 · class (internal sealed)

  • What it is: a DelegatingHandler that stamps the bearer token and the x-goog-user-project quota-project header on every OTLP export request (GoogleCloudTelemetryAuthHandler.cs:14-18).
  • Depends on: GoogleCloudTelemetryAccessToken; BCL DelegatingHandler, AuthenticationHeaderValue.
  • Concept: fail closed on a missing credential. [Rubric §11, Security]: with no token yet, the handler throws HttpRequestException with HttpStatusCode.ServiceUnavailable (:46-52), which the OTLP exporter records as a failed export, so no request without a credential ever leaves the process (:36-40).
  • Walkthrough: QuotaProjectHeader constant (:18). Both Send (:22-26) and SendAsync (:30-34) call Authorize and delegate. Authorize (:41-57) reads accessToken.Current, sets Authorization: Bearer {token} (:54), and replaces any existing quota-project header with the configured project (:55-56).
  • Why it's built this way: overriding the synchronous Send too covers OTLP exporter paths that send synchronously; both paths share one Authorize. Telemetry rationale: ADR-041.
  • Where it's used: constructed per exporter HttpClient in GoogleCloudTelemetryExporterExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudTelemetryExporterExtensions.cs:203-207). Tested in MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Telemetry/GoogleCloudTelemetryAuthHandlerTests.cs.

GoogleSecretManagerConfigurationExtensions

MMCA.Common.Gcp · MMCA.Common.Gcp · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleSecretManagerConfigurationExtensions.cs:23 · Level 2 · class (static, extension<TBuilder>)

  • What it is: AddGoogleSecretManagerConfiguration(), which adds a Google Cloud Secret Manager configuration source so every secret under one id prefix reads through IConfiguration (GoogleSecretManagerConfigurationExtensions.cs:31-33).
  • Depends on: GoogleSecretManagerConfigurationSource, HostIntegrationMarker, HostIntegrationNames; external SecretManagerServiceClient.
  • Concept: the secrets-as-configuration convention taught at AwsSecretsManagerConfigurationProvider, on Google. [Rubric §33, Developer Experience]: the naming is the Key Vault one, so a secret set moves between clouds by renaming its prefix alone: adc-prod-ConnectionStrings--Default arrives as ConnectionStrings:Default (:48-55). The latest version is read; a disabled, destroyed or version-less secret is skipped (:53-55).
  • Walkthrough: SectionPath is "GoogleSecretManager" (:25). Keys (:37-44): ProjectId and NamePrefix together are the gate, ReloadIntervalMinutes optional. An incomplete gate returns with no receipt (:88-95). The reload interval is parsed and a bad value throws (:100-112), as in AzureKeyVaultConfigurationExtensions. Then the receipt (:116) and configuration.Add(new GoogleSecretManagerConfigurationSource(SecretManagerServiceClient.Create, projectId, namePrefix, reloadInterval)) (:118-122).
  • Why it's built this way: not called from AddServiceDefaults() for the Key Vault reason, and read synchronously at the call, so it belongs early in the host builder (:67-73). Required IAM (secretmanager.secrets.list, secretmanager.versions.get, secretmanager.versions.access) is stated at :58-64; a secret the identity cannot read fails startup, naming it (:64). Recorded in ADR-061 and ADR-134.
  • Where it's used: no call site in MMCA.ADC/Source; the tripwire message naming it is at MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Integrations/HostIntegrationTripwire.cs:91.

GoogleCloudTelemetryExporterExtensions

MMCA.Common.Gcp · MMCA.Common.Gcp · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudTelemetryExporterExtensions.cs:20 · Level 3 · class (static, extension<TBuilder>)

  • What it is: AddGoogleCloudTelemetryExporter(), which exports traces and logs (not metrics) to the Google Cloud Telemetry API over OTLP HTTP/protobuf when GoogleCloudTelemetry:ProjectId is set, landing them in Cloud Trace and Cloud Logging (GoogleCloudTelemetryExporterExtensions.cs:44-47).
  • Depends on: IGoogleCloudTelemetryTokenSource, ApplicationDefaultTelemetryTokenSource, GoogleCloudTelemetryAccessToken, GoogleCloudTelemetryAuthHandler, HostIntegrationMarker, HostIntegrationNames; externals OtlpTraceExporter, OtlpLogExporter, BatchActivityExportProcessor, BatchLogRecordExportProcessor, OtlpExporterOptions.
  • Concept: a second vendor exporter beside the vendor-neutral one. [Rubric §13, Observability & Operability]: the same ordering precondition as AzureMonitorExporterExtensions is enforced (:103-111). [Rubric §31, Cost/FinOps] and §13 together explain why metrics are excluded: the Telemetry API takes at most 200 points per metric request and the .NET SDK cannot split one metric across requests, so metrics go through a Google-built Collector with send_batch_max_size: 200 instead (:78-86).
  • Walkthrough:
    • Constants (:23-38): ProjectIdKey, EndpointKey, DefaultEndpoint (https://telemetry.googleapis.com/), and ProjectIdAttribute = gcp.project_id, the resource attribute Google maps data to a project with (:31-37).
    • Gate (:113-120): a blank project id registers nothing, no receipt.
    • Registration (:122-131): trims the id, resolves the endpoint, registers the receipt, TryAddSingleton of the token source, the access-token singleton and its hosted-service registration.
    • Pipeline (:137-144): adds the gcp.project_id resource attribute and hand-built batch processors for v1/traces and v1/logs. The comment at :133-136 explains why not AddOtlpExporter: the SDK refuses a signal-specific AddOtlpExporter where UseOtlpExporter (enabled by service defaults) also runs.
    • ResolveEndpoint (:157-175): default when blank, throws on a non-absolute or non-http(s) value (:165-169), and appends a trailing slash so new Uri(root, "v1/traces") does not replace the last segment (:172-174). SignalEndpoint is that Uri combine (:181).
    • CreateExporterOptions (:193-209): full endpoint URL, HttpProtobuf, Headers = null (:200) so an OTEL_EXPORTER_OTLP_HEADERS meant for the vendor-neutral exporter never reaches Google (:188-191), and an HttpClientFactory wrapping GoogleCloudTelemetryAuthHandler with the exporter's timeout (:203-207).
  • Why it's built this way: credentials load at host start, not at registration, and a host without them still starts with exports refused locally (:62-67). Resource mapping (generic_task versus generic_node) is left to OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES (:70-75). Recorded in ADR-041 and ADR-134.
  • Where it's used: no call site in MMCA.ADC/Source; tripwire message at MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Integrations/HostIntegrationTripwire.cs:101. Tested by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Telemetry/GoogleCloudTelemetryExporterExtensionsTests.cs and GoogleCloudTelemetryExportTests.cs.

GcsFileStorageService

MMCA.Common.Gcp · MMCA.Common.Gcp.Storage · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Storage/GcsFileStorageService.cs:25 · Level 4 · class (public sealed)

  • What it is: the Google Cloud Storage implementation of IFileStorageService: upload returns the object URL, delete is delete-if-exists (GcsFileStorageService.cs:25-28).
  • Depends on: IFileStorageService, FileUploadOptions, GcsFileStorageSettings, GcsObjectUrl, Result, Error; externals StorageClient, StorageObject, GoogleApiException.
  • Concept: one port, three vendor adapters with matching error contracts, as with AzureBlobFileStorageService and S3FileStorageService. [Rubric §1, SOLID] (dependency inversion): callers depend on the core interface and never see Google types. Failures map to Result with stable codes FileStorage.UploadFailed / FileStorage.DeleteFailed, not exceptions (see the Result pattern).
  • Walkthrough:
    • IsConfigured is true (:33). The short UploadAsync overload forwards with FileUploadOptions.None (:37-38).
    • UploadAsync (:42-84): composes the URL first so a configuration that cannot name the object leaves no orphan (:46-48); builds a StorageObject whose ContentDisposition and CacheControl are set only when non-empty, stored as object metadata (:50-59); uploads without closing the caller's stream (:63-65). GoogleApiException and transport failures log and return UploadFailed (:68-83).
    • DeleteAsync (:87-116): a 404 GoogleApiException is success (:94-99), mirroring DeleteBlobIfExists; other API and transport failures return DeleteFailed.
    • IsTransportFailure (:124-126): HttpRequestException, IOException, or an OperationCanceledException the caller did not request; the caller's own cancellation still propagates.
  • Why it's built this way: ADR-045 defines the storage port; the RS0026 suppressions document that the overload pair mirrors the grandfathered interface (:36, :41).
  • Where it's used: registered by GcpServiceCollectionExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:62). Tested by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsFileStorageServiceTests.cs; also referenced by CloudVendorRulesTests.cs in the same project.

AzureServiceCollectionExtensions

MMCA.Common.Azure · MMCA.Common.Azure · MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:28 · Level 5 · class (static, extension(IServiceCollection))

  • What it is: the Azure adapter's DI entry points: AddAzureBlobFileStorage, AddAzureNotificationHubsPush, and AddAzureServiceBusTransport (AzureServiceCollectionExtensions.cs:41, :83, :117).
  • Depends on: AzureBlobFileStorageSettings, AzureBlobFileStorageService, AzureNotificationHubSettings, AzureNotificationHubNativePushSender, AzureNotificationHubDeviceRegistrar, AzureServiceBusTransport, IFileStorageService, INativePushSender, IPushDeviceRegistrar, IBrokerTransport; externals BlobServiceClient, DefaultAzureCredential, NotificationHubClient.
  • Concept: config-gated, unconditionally-callable registrations through extension(IServiceCollection) members (C# extension(T) types, ADR-106). [Rubric §7, Microservices Readiness] assesses whether infrastructure is swappable per deployment: each call is a no-op when its section is incomplete, so hosts call it always and deployments switch it on by configuration alone.
  • Walkthrough:
    • AddAzureBlobFileStorage (:41-71): binds options (:45-46), returns when ContainerName is blank (:49-52) or when there is neither an absolute ServiceUri nor a connection string (:54-59; an empty-string ServiceUri binds as a relative Uri, :54). Registers a BlobContainerClient with TryAddSingleton, preferring ServiceUri + DefaultAzureCredential over the connection string (:61-67), then the transient service (:68).
    • AddAzureNotificationHubsPush (:83-105): binds options, requires Enabled, ConnectionString and HubName (:91-96), registers INotificationHubClient via TryAddSingleton and the sender and registrar (:98-102), replacing the core null pair (ADR-044).
    • AddAzureServiceBusTransport (:117-118): services.AddBrokerTransport(new AzureServiceBusTransport()); the doc comment says call it BEFORE AddBrokerMessaging, which selects the transport by MessageBus:Provider=AzureServiceBus (ADR-066).
  • Why it's built this way: ADR-045, ADR-070 and ADR-134 place vendor code in adapters over core ports. The TryAdd client registration lets a host supply its own client first.
  • Where it's used: MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:261 and MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:367 (blob storage); MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.API/DependencyInjection.cs:40 (push); every ADC service host for Service Bus (for example MMCA.ADC/Source/Services/MMCA.ADC.Engagement.Service/Program.cs:298).

GcpServiceCollectionExtensions

MMCA.Common.Gcp · MMCA.Common.Gcp · MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:26 · Level 8 · class (static, extension(IServiceCollection))

  • What it is: the GCP adapter's DI entry points: AddGoogleCloudStorage, AddCloudSqlIamAuthentication, and AddFirebaseCloudMessagingPush (GcpServiceCollectionExtensions.cs:48, :90, :142).
  • Depends on: GcsFileStorageSettings, GcsStorageClientFactory, GcsFileStorageService, CloudSqlIamAuthenticationSettings, CloudSqlIamAuthenticationConfigurator, FcmNativePushSettings, FcmPushClientFactory, IFcmMessagingClient, IFcmInstallationRegistry, FirestoreInstallationRegistry, FcmNativePushSender, FcmPushDeviceRegistrar, IFileStorageService, INativePushSender, IPushDeviceRegistrar, INpgsqlDataSourceConfigurator.
  • Concept: the same gated, unconditionally-callable shape as AzureServiceCollectionExtensions and AwsServiceCollectionExtensions, with fail-fast validation at registration. [Rubric §11, Security]: every GCP path authenticates with Application Default Credentials, with no key-file or HMAC setting (:36-37, :124-126), and Cloud SQL IAM auth refuses connection strings that would send the token in clear.
  • Walkthrough:
    • AddGoogleCloudStorage (:48-65): binds FileStorage (:52-53) and returns when GcsBucket is blank (:56-59), even if the section carries the S3 Bucket or Azure ContainerName (:38-39). Registers the client through TryAddSingleton(_ => GcsStorageClientFactory.Create(settings)) (:61), so a host-registered StorageClient wins, and the transient service (:62).
    • AddCloudSqlIamAuthentication (:90-105): gated on CloudSql:IamAuthentication:Enabled (:94-98); calls EnsureConfiguredConnectionStringsAreEncrypted so a non-TLS PostgreSQL connection string fails at registration (:100-102), then AddNpgsqlDataSourceConfigurator(new CloudSqlIamAuthenticationConfigurator()) (:104). The doc comment records the token scope, the 1-minute refresh and 30-second failure retry, no password in the connection string, and the --auto-iam-authn proxy alternative (:67-84); SQL Server, SQLite and Cosmos are untouched.
    • AddFirebaseCloudMessagingPush (:142-166): requires NativePush:Enabled true and a complete NativePush:Fcm section (:146-153), validates the broadcast topic (:156), then binds options and registers Firestore, the messaging client, the installation registry, the sender and the registrar (:158-163). The doc comment explains the Firestore installation registry, topic broadcast, and that raw APNs tokens are rejected with PushDevice.UnsupportedPlatform (:107-136).
  • Why it's built this way: ADR-045 (storage), ADR-044 (push), ADR-113 (PostgreSQL), ADR-070 (fail-fast configuration), ADR-106 and ADR-134.
  • Where it's used: no call site in MMCA.ADC/Source. Tested by MMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/GcpServiceCollectionExtensionsTests.cs.

⬅ Common AI Integration • Index • Testing & Quality Infrastructure ➡