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 isNullFileStorageService(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) andIPushDeviceRegistrar(MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/Notifications/IPushDeviceRegistrar.cs:11) (ADR-044). The core defaults areNullNativePushSenderandNullPushDeviceRegistrar(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 throughAddBrokerTransport(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 inBrokerTransportPolicies. - PostgreSQL connection customization:
INpgsqlDataSourceConfigurator(MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/DbContexts/INpgsqlDataSourceConfigurator.cs:23), added throughAddNpgsqlDataSourceConfigurator(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 thatAddServiceDefaults()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.
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:AmazonSqssection (AmazonSqsTransportSettings.cs:11). - Depends on: nothing first-party;
System.Urifor 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 sharedMessageBuskeys stay transport-agnostic. - Walkthrough: four
initproperties.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 byAmazonSqsTransport).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 byAwsStaticCredentials). - 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
ClientConfigat either a custom endpoint or an AWS region, from theServiceUrl/Regionpair every AWS settings section in this package carries. - Depends on: nothing first-party; AWS SDK
ClientConfigandRegionEndpoint. - 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 absoluteserviceUrlwins: it setsconfig.ServiceURL(AwsClientEndpoint.cs:29) and, when a region is also given, uses it as the signing region viaAuthenticationRegion(AwsClientEndpoint.cs:32). Otherwise a non-blank region setsRegionEndpoint(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, andAwsSecretsManagerConfigurationExtensions(MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsSecretsManagerConfigurationExtensions.cs:118).AmazonSqsTransportdoes 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/SecretAccessKeypair. - 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 aBasicAWSCredentialswhen both halves are set (AwsStaticCredentials.cs:27-29), throwsInvalidOperationExceptionnaming both keys undersectionPathwhen exactly one is set (AwsStaticCredentials.cs:32-35), and returnsnullwhen 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) andS3StorageClientFactory.
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 theAwsStaticCredentialsboth-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);SectionPathis quoted in the TLS error ofRdsIamAuthenticationConfigurator(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 optionalSubscriptionArn(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;
SubscriptionArnis the only nullable member because a row can exist before or without a topic subscription.UserIdholds the invariant string form produced bySnsInstallationRegistry.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 byTryPutAsync(SnsInstallationRegistry.cs:75), built bySnsPushDeviceRegistrar(MMCA.Common/Source/Hosting/MMCA.Common.Aws/Notifications/SnsPushDeviceRegistrar.cs:170), and grouped by endpoint inSnsNativePushSender(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:
HostIntegrationMarkerandHostIntegrationNames; externalsIHostApplicationBuilder,AddDataProtection, and the AWSPersistKeysToAWSSystemsManagerextension. - 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 registersHostIntegrationMarker(HostIntegrationNames.AwsDataProtection)(AwsDataProtectionExtensions.cs:72), resolves the application name fromDataProtection:ApplicationNameor the host'sApplicationName(AwsDataProtectionExtensions.cs:74-75), and callsPersistKeysToAWSSystemsManager(prefix, ...)(AwsDataProtectionExtensions.cs:80), settingKMSKeyIdonly whenDataProtection:KmsKeyIdis 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 withDataProtection:BlobStorageUrifails the startup tripwire (AwsDataProtectionExtensions.cs:52-53), one key ring per host. Recorded in ADR-069 and ADR-134; the Azure sibling isAzureDataProtectionExtensions. - Where it's used: no call site outside the defining file; no
MMCA.ADC/Sourcehost 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 keyNativePush:Enabled(SnsNativePushSettings.cs:18). - Depends on:
DeviceInstallationRequestfor the normalized platform constants. - Concept: cross-reference
AwsServiceCollectionExtensions(AddAmazonSnsPush) for the gate-plus-completeness registration pattern this class feeds. - Walkthrough:
FcmPlatformApplicationArnandApnsPlatformApplicationArn(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) mapsDeviceInstallationRequest.FcmV1Platform/ApnsPlatformto the configured ARN, returningnullfor 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 throughIOptions<SnsNativePushSettings>inSnsInstallationRegistry,SnsNativePushSenderandSnsPushDeviceRegistrar.
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:
NativePushPayloadsfor 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 thefcmV1Messageenvelope),APNSandAPNS_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
defaultentry, and thatdefaultis 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:
SnsNativePushSenderfor 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 throughIConfigurationlike any other setting. - Depends on:
AwsSecretsManagerConfigurationSource,AwsStaticCredentials,AwsClientEndpoint,HostIntegrationMarker,HostIntegrationNames; AWS SDKAmazonSecretsManagerClient. - 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 anAmazonSecretsManagerConfigthroughAwsClientEndpoint.Apply(AwsSecretsManagerConfigurationExtensions.cs:117-121), registersHostIntegrationMarker(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, soadc/prod/ConnectionStrings--Defaultarrives asConnectionStrings: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 fromAddServiceDefaults(), and becauseConfigurationManagerloads 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/Sourcehost 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
INpgsqlDataSourceConfiguratorthat 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; NpgsqlNpgsqlDataSourceBuilder.UsePeriodicPasswordProvider, AWS SDKRDSAuthTokenGenerator. - 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
regionandcredentials, both nullable (RdsIamAuthenticationConfigurator.cs:20).TokenRefreshIntervalis 10 minutes (RdsIamAuthenticationConfigurator.cs:24) andFailureRetryInterval30 seconds (RdsIamAuthenticationConfigurator.cs:27).Configure(builder)(RdsIamAuthenticationConfigurator.cs:31) checks TLS on the builder's connection string (RdsIamAuthenticationConfigurator.cs:35-37) and installsUsePeriodicPasswordProvider(GenerateTokenAsync, ...)(RdsIamAuthenticationConfigurator.cs:39).EnsureConfiguredConnectionStringsAreEncrypted(configuration)(RdsIamAuthenticationConfigurator.cs:50) checks the top-levelConnectionStrings:PostgreSQLConnectionStringand everyDataSources:{name}:PostgreSQLConnectionStringalready in configuration (RdsIamAuthenticationConfigurator.cs:54-59), parsing each throughNpgsqlDataSourceBuilderso it sees whatConfigurewill see (RdsIamAuthenticationConfigurator.cs:65-66).GenerateTokenAsync(RdsIamAuthenticationConfigurator.cs:75) requires aHostandUsername(RdsIamAuthenticationConfigurator.cs:82-86), then picks one of fourRDSAuthTokenGenerator.GenerateAuthTokenAsyncoverloads by which of credentials and region are set (RdsIamAuthenticationConfigurator.cs:90-105).EnsureEncrypted(RdsIamAuthenticationConfigurator.cs:108) acceptsRequire,VerifyCAorVerifyFull(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, whileConfigurestill 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 byMMCA.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
SnsNativePushSettingsand optional static credentials. - Depends on:
SnsNativePushSettings,AwsClientEndpoint; AWS SDK SNS and DynamoDB clients. - Concept: cross-reference
AwsClientEndpoint; the same factory shape asS3StorageClientFactory. - Walkthrough:
CreateSns(SnsPushClientFactory.cs:18) andCreateDynamoDb(SnsPushClientFactory.cs:34) are symmetric: a fresh config,AwsClientEndpoint.Apply(config, settings.ServiceUrl, settings.Region)(SnsPushClientFactory.cs:23,39), then the client with or withoutcredentials(SnsPushClientFactory.cs:25-27,41-43). OneServiceUrltherefore redirects both services to the same emulator. - Where it's used: the
TryAddSingletonfactories inAddAmazonSnsPush(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 byMessageBus: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; MassTransitUsingAmazonSqs, AWS SDKRegionEndpoint. - Concept: a pluggable transport strategy; cross-reference
AzureServiceBusTransportfor 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 inArchitectureRulesforbidMassTransit.AmazonSqsTransportandMMCA.Common.Awsfrom 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) andAmbientHeaderPrefix = "MMCA-"(AmazonSqsTransport.cs:46);DefaultRedeliveryIntervalsSeconds = [60, 300, 900](AmazonSqsTransport.cs:55). The public constructor falls back toFallbackRegionFactory.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 whileAddBrokerMessagingruns (AmazonSqsTransport.cs:81-88). InsideUsingAmazonSqsit 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), thenBrokerTransportPolicies.ApplyReceivePoliciesandConfigureEndpoints(AmazonSqsTransport.cs:109-110).ResolveRedeliveryIntervals(AmazonSqsTransport.cs:124) defers toBrokerTransportPolicies.BuildRedeliveryIntervalswith the SQS default and thenEnsureWithinDelayCap(AmazonSqsTransport.cs:149), which throws for any interval over 900 seconds (AmazonSqsTransport.cs:153-161).IsTransportHeader(AmazonSqsTransport.cs:139-140) excludesMMCA-*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, soMessageBus:EnableDelayedRedelivery(a RabbitMQ plugin gate) is not consulted (AmazonSqsTransport.cs:18-21). TheMMCA-*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 byMMCA.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 SDKIAmazonDynamoDBandIAmazonSimpleNotificationService. - Concept introduced, ownership rules as conditional writes. The
IPushDeviceRegistrarcontract 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 DynamoDBConditionExpression,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 withConsistentRead = true(SnsInstallationRegistry.cs:64) because ownership decisions follow.TryPutAsync(SnsInstallationRegistry.cs:75) writes all attributes (omitting a blankSubscriptionArn,SnsInstallationRegistry.cs:85-88) under the ownership condition (SnsInstallationRegistry.cs:97) and returnsfalseonConditionalCheckFailedException(SnsInstallationRegistry.cs:107-110).QueryByUserAsync(SnsInstallationRegistry.cs:117) pages the GSI untilLastEvaluatedKeyis empty (SnsInstallationRegistry.cs:145-150).DeleteOwnedAsync(SnsInstallationRegistry.cs:162) andDeleteIfEndpointAsync(SnsInstallationRegistry.cs:173) are conditional deletes throughDeleteWhenAsync(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, treatingNotFoundExceptionas done (SnsInstallationRegistry.cs:190-195), then deletes the endpoint (SnsInstallationRegistry.cs:198).FromItem(SnsInstallationRegistry.cs:227) returnsnullfor 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 bySnsNativePushSenderandSnsPushDeviceRegistrar. - Caveats / not-in-source: the table and its
UserId-indexGSI 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,LoggerMessagesource 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 onAmazonServiceException(SnsNativePushSender.cs:50-56), groups rows byEndpointArnso 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 toBroadcastTopicArn(SnsNativePushSender.cs:76).TryPublishAsync(SnsNativePushSender.cs:85) targets the endpoint (SnsNativePushSender.cs:92);EndpointDisabledExceptionorNotFoundExceptiontriggers stale cleanup (SnsNativePushSender.cs:99-102) and any otherAmazonServiceExceptionis 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
INativePushSenderbyAddAmazonSnsPush(MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:184), replacingNullNativePushSender; covered byMMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Notifications/SnsNativePushSenderTests.cs. - Caveats / not-in-source:
BroadcastAsynchas 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
SnsInstallationRegistryfor the ownership rule, andAzureNotificationHubDeviceRegistrar, whose rule this one states it mirrors (SnsPushDeviceRegistrar.cs:129-130). Errors come back asResultfailures, 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) normalizesfcmv1/apnscase-insensitively (SnsPushDeviceRegistrar.cs:43-48), returnsPushDevice.UnsupportedPlatformvalidation failures for an unknown platform (SnsPushDeviceRegistrar.cs:51-54) or one without a configured platform application (SnsPushDeviceRegistrar.cs:57-64), and mapsAmazonServiceExceptiontoPushDevice.UpsertFailed(SnsPushDeviceRegistrar.cs:71-77).RegisterAsync(SnsPushDeviceRegistrar.cs:139) reads the row, returnsPushDevice.InstallationConflict(SnsPushDeviceRegistrar.cs:122-126) whenClaimedByAnotherDeviceis 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 protocolapplication(SnsPushDeviceRegistrar.cs:153-168), and writes the row withTryPutAsync(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 viaTryRefreshEndpointAsync, which setsTokenandEnabled = "true"and returnsfalseonNotFoundException(SnsPushDeviceRegistrar.cs:221-242); otherwise it callsCreatePlatformEndpoint(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 toPushDevice.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).CreatePlatformEndpointis 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
IPushDeviceRegistrarbyAddAmazonSnsPush(MMCA.Common/Source/Hosting/MMCA.Common.Aws/AwsServiceCollectionExtensions.cs:185), replacingNullPushDeviceRegistrar; covered byMMCA.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
CreatePlatformEndpointhad 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,AddAwsRdsIamAuthenticationandAddAmazonSnsPush, 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, andArchitectureRules.CloudVendorsenforces 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) bindsFileStorageoptions (AwsServiceCollectionExtensions.cs:54-55), is a no-op without aBucket(AwsServiceCollectionExtensions.cs:57-61), checks the key pair at registration (AwsServiceCollectionExtensions.cs:64), thenTryAddSingletons the S3 client and addsIFileStorageServiceasS3FileStorageService(AwsServiceCollectionExtensions.cs:66-67). Checksum settings default toWHEN_REQUIREDbecause 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 newAmazonSqsTransportthroughAddBrokerTransport; it must be called beforeAddBrokerMessaging, which picks the transport (AwsServiceCollectionExtensions.cs:74-75). See ADR-066.AddAwsRdsIamAuthentication(configuration)(AwsServiceCollectionExtensions.cs:107) is a no-op unlessRds:IamAuthentication:Enabledis 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 callsAddNpgsqlDataSourceConfiguratorwith aRdsIamAuthenticationConfigurator(AwsServiceCollectionExtensions.cs:128).AddAmazonSnsPush(configuration)(AwsServiceCollectionExtensions.cs:164) is a no-op unlessNativePush:Enabledparses true and the sectionIsComplete(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:
TryAddSingletonfor 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, inAwsSecretsManagerConfigurationExtensionsandAwsDataProtectionExtensions, because they act onbuilder.Configuration. - Where it's used: no call site outside the defining file; no
MMCA.ADC/Sourcehost calls any of the four methods (ADC runs on Azure). Tests call them, for exampleMMCA.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
ConfigurationProviderthat reads every AWS Secrets Manager secret under one name prefix intoIConfiguration, 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, soadc/prod/ConnectionStrings--Defaultarrives asConnectionStrings: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 BCLPeriodicTimer/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 inLastReloadFailureinstead 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 = 20is the most ids oneBatchGetSecretValuecall accepts (:30);KeyDelimiteris"--"(:32). The client, prefix, optional reload interval, a disposalCancellationTokenSourceand the polling task are the fields (:34-38). The constructor null-guards the client and prefix (:44-49). KeyFor(secretName, namePrefix)(:61-69) returnsnullwhen the name is outside the prefix (an ordinalStartsWith) or equals the prefix exactly, otherwise the suffix with every--replaced byConfigurationPath.KeyDelimiter.Load()(:72-86) blocks once onLoadAsync, under a scopedVSTHRD002suppression (:78-80). The comment explains why the wait is safe:ConfigurationProvider.Loadis 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 startsPollAsync(:82-85).LoadAsync(:132-143) lists names, then reads them in chunks ofBatchSizeinto aDictionarykeyed withStringComparer.OrdinalIgnoreCase(:135,:137).ListNamesAsync(:145-172) pagesListSecretswith aNamefilter on the prefix (:154) and re-checks every name throughKeyFor(: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) pagesBatchGetSecretValue. Any per-secret error throws anInvalidOperationExceptionthat lists each secret id, error code and message and states the fix: grantsecretsmanager:GetSecretValue(andkms:Decrypton a customer-managed key) or move the secret out of the prefix (:183-188). OnlySecretStringvalues are kept, so a binary-only secret is skipped (:191-196).PollAsync/ReloadOnceAsync(:96-110,:112-130) tick aPeriodicTimer; each tick reloads, clearsLastReloadFailure, and only whenHasSameData(:204-206) reports a difference swapsDataand callsOnReload(), 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.
- Constants and state.
- 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
OnReloadkeep the provider from importing a wrong-environment secret or churning everyIOptionsMonitoron 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 byMMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Secrets/AwsSecretsManagerConfigurationProviderTests.cs. - Caveats / not-in-source: no host in
MMCA.ADC/SourcecallsAddAwsSecretsManagerConfiguration, 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
FileStoragesection 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.Urionly. - 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:ServiceUriis the managed-identity path (DefaultAzureCredential), andConnectionStringis documented as the local-development (Azurite) alternative (:15-19). - Walkthrough:
SectionNameis"FileStorage"(:13);ServiceUri(:16),ConnectionString(:19) andContainerName(:22, documented as required) areinit-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
FileStoragesection 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
.AddAzureBlobFileStoragebinds it (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:45-48) and registers nothing whenContainerNameis blank or neither an absoluteServiceUrinor aConnectionStringis present (:49-59); the comment at:54records the trap that an empty-stringServiceUribinds to a relativeUri, so onlyIsAbsoluteUricounts.
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
NativePushsection that switches Azure Notification Hubs delivery on: anEnabledflag, 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:
SectionNameis"NativePush"(:12);Enabled(:15),ConnectionString(:18, documented as a Listen+Send+Manage rule) andHubName(:21) areinit-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:
AddAzureNotificationHubsPushbinds it (MMCA.Common/Source/Hosting/MMCA.Common.Azure/AzureServiceCollectionExtensions.cs:87-90) and returns without registering anything unlessEnabledis 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
UserIdentifierTypealias; BCLstring.CreatewithCultureInfo.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).BuildUserTagExpressionsmaps every id throughUserTag, chunks by 20, and joins each chunk with" || "(:17-21).UserTagPrefixis"user:"(:24), andUserTagformats 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 stampsUserTagon each installation and usesUserTagPrefixto recognise another owner (MMCA.Common/Source/Hosting/MMCA.Common.Azure/Notifications/AzureNotificationHubDeviceRegistrar.cs:41,:125-128). Covered byMMCA.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
FileStoragesection 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 toWHEN_REQUIREDinstead of the AWS SDK v4 defaultWHEN_SUPPORTED, because the v4 SDK attaches CRC32 trailers andx-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:
SectionNameis"FileStorage"(:16).Bucketis required and provisioned by infrastructure (:19).Regionis the AWS region, or the signing region whenServiceUrlis set (R2 expectsauto), and falls back to the SDK chain when blank (:21-26).ServiceUrlselects an S3-compatible endpoint (:28-33);ForcePathStyleswitches to{endpoint}/{bucket}/{key}addressing, needed by MinIO and LocalStack (:35-39).PublicBaseUrloverrides the returned URL, for a CDN or R2 public domain (:41-46).AccessKeyId/SecretAccessKeyare an optional static pair; setting only one fails registration (:48-56).RequestChecksumCalculationandResponseChecksumValidationdefault toWHEN_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
FileStoragesection 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 whenBucketis blank (:58-61) and resolves the key pair at registration (:64). Read by S3StorageClientFactory, S3ObjectUrl and S3FileStorageService; tested acrossMMCA.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'sIServiceBusBusFactoryConfigurator(ServiceBusEmulatorSupport.cs:1-4). BCL:Uri,Interlocked,CultureInfo. Its one caller is AzureServiceBusTransport.Configure, and its input comes from MessageBusSettings (ConnectionStringplusEmulatorAdminEndpoint).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 carriesUseDevelopmentEmulator=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-clientsHostoverload, where the caller builds both the data-plane and the management-plane client itself (ServiceBusEmulatorSupport.cs:19-30).Walkthrough:
- The four pieces of state.
EmulatorMarkeris the literal"UseDevelopmentEmulator=true"(ServiceBusEmulatorSupport.cs:37).EmulatorEntityQuotais one hour, the ceiling the emulator enforces on entity time-to-live and auto-delete-on-idle (:42).EmulatorHostAddressissb://localhost/(:48) and the doc is careful about what it is: it names the bus for theHostoverload, not a network location, because both clients are already bound to the emulator's actual ports (:44-47)._entityQuotasAppliedis the once-per-process latch (:54). IsEmulatorConnectionString(:62-64) is the whole detection: a null-guard and aContainswithStringComparison.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 newServiceBusClientfor the data plane and a newServiceBusAdministrationClientfor the management plane (:97-100). TheCA2000suppression 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) flipsInterlocked.Exchange(ref _entityQuotasApplied, 1) != 0and returns early on the second call (:110-113), then writes three MassTransit statics down to the one-hour quota:DefaultMessageTimeToLive,BasicMessageTimeToLiveandAutoDeleteOnIdle(: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:5300is an absolute URI, withlocalhostread as the scheme and no host at all, so the check also requires the scheme to behttporhttps(:136-139). A missing or malformed endpoint throws anInvalidOperationExceptionwhose 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 theEndpoint=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).
- The four pieces of state.
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
.ConfigurecallsIsEmulatorConnectionStringand, on a match,ConfigureEmulatorHost(cfg, connectionString, settings.EmulatorAdminEndpoint)(MMCA.Common/Source/Hosting/MMCA.Common.Azure/Messaging/AzureServiceBusTransport.cs:41-45, the productioncfg.Host(connectionString)in theelseat:48). The admin endpoint is bound fromMessageBus: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.cscovers 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
IConfigurationSourcethat 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 aFunc<IAmazonSecretsManager>rather than a client lets a test hand in a fake, and gives eachBuilda fresh client that the provider owns and disposes. - Walkthrough: a primary constructor over
clientFactory,namePrefixandreloadInterval(:14-17);NamePrefix(:20) andReloadInterval(:23) are exposed internally so tests can assert what the extension configured;Buildcalls 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 whenSecretsManager:NamePrefixis set (:85-94), with a static key pair when one is configured (:97-98), and aReloadIntervalMinutesthat must parse as a positive whole number or the call throws (:103-115). Asserted inMMCA.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.INotificationHubClientandILogger<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 anFcmV1Notificationsend and anAppleNotificationsend 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 hasEnabled: 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 byMMCA.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. APublicBaseUrlthat is absolute wins:{PublicBaseUrl}/{key}, without repeating the bucket (:36-39). An absoluteServiceUrlgives{ServiceUrl}/{bucket}/{key}whenForcePathStyleis set (:45-48), otherwise aUriBuilderprepends 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 anInvalidOperationExceptionnaming the three settings that would fix it (:54-58).EscapeKeysplits on/, appliesUri.EscapeDataStringto each segment and rejoins, so separators survive and spaces or non-ASCII characters do not break the URL (:65-66).TrimEnddrops 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
.UploadAsynccomposes 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
AmazonS3Configand creates theIAmazonS3client, 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) copiesForcePathStyleand both checksum modes (:27-32), then letsAwsClientEndpoint.Applychoose between a service URL with a signing region and a plain region (:34).Create(:45-53) resolves credentials (:48) and constructsAmazonS3Clientwith 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:
AddS3FileStorageresolves the key pair againstSectionPathat registration, so a half-configured pair fails there rather than on the first upload, then registersCreateas theIAmazonS3singleton withTryAddSingleton, 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.BlobContainerClientandILogger<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. ACreateIfNotExistsin 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
IsConfiguredis a constanttrue(AzureBlobFileStorageService.cs:25). Its purpose is to differ from thefalseon NullFileStorageService.UploadAsync(blobName, content, contentType, ct)(:29-30) forwards to the options overload withFileUploadOptions.None. Both overloads carry anRS0026suppression (:28,:33): the pair is grandfathered on the interface, and the justification records that the type moved here fromMMCA.Common.Infrastructure, so both overloads are unshipped in this package.- The real work is
UploadAsync(blobName, content, contentType, options, ct)(:34-72). It null-guardsoptions(:36), resolves a blob client (:40), and uploads withBlobUploadOptionscarrying the content type plusoptions.ContentDispositionandoptions.CacheControlas HTTP headers (:41-52), returning the blob's URI on success (:54). - Two catches yield the same
Error.Failurewith codeFileStorage.UploadFailed:RequestFailedException, the Azure SDK's service failure (:56-63), and any exceptionIsTransportFailureaccepts (:64-71). IsTransportFailure(:107-109) acceptsHttpRequestException,IOException, or anOperationCanceledExceptionthe 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) usesDeleteBlobIfExistsAsync(: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 yieldFileStorage.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 absoluteServiceUriwithDefaultAzureCredentialis the managed-identity production path,ConnectionStringis the local Azurite path, andContainerNameis required for either (:49-66). An incomplete section returns before registering anything, which leaves the null default in place, so a host can callAddAzureBlobFileStorageunconditionally. - 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 byMMCA.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:
INotificationHubClientandILogger<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 asksClaimedByAnotherDeviceAsync(: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:FCMV1andAPNS, compared afterToUpperInvariant, map toNotificationPlatform.FcmV1and.Apns, and anything else yieldsnull(: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
Installationis assembled with the client-supplied id, the resolved platform, the push channel, and exactly one tag, the owner stamp fromNativePushUserTags.UserTag(:36-42). Before writing, the upsert callsClaimedByAnotherDeviceAsync; when it returns true the result isError.Conflictwith codePushDevice.InstallationConflict(:54-60).CreateOrUpdateInstallationAsyncis 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 (aMessagingEntityNotFoundExceptionon the read means free,:120-123), it carries auser:-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 toPushDevice.UpsertFailedafter 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-83settles 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.
MessagingEntityNotFoundExceptionreturns success, because an unknown installation is already the desired state (:95-99); a generalMessagingExceptionlogs and returnsPushDevice.DeleteFailed(:100-104). OwnedBy(:133-134) checks the tags contain the owner tag withStringComparer.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
AddAzureNotificationHubsPushalongside 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 byMMCA.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-88says. 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:ProviderisAzureServiceBus(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 byName(:22).Configure(:25-55) null-guards both arguments (:27-28), reads settings and connection string from the context (:30-31), and insideUsingAzureServiceBus(:33) does three things. With a non-blank connection string it either hands off toServiceBusEmulatorSupport.ConfigureEmulatorHostwhen the emulator marker is present (:41-45) or callscfg.Host(connectionString)(:48). It then applies the shared receive policies with delayed redelivery enabled (:52) and callsConfigureEndpoints(: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 namingMassTransit.AzureServiceBusTransportamong 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 beforeAddBrokerMessaging(:107-115). Every MMCA.ADC service host calls it (for exampleMMCA.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 byMMCA.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>andILogger<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 inSendsUnsignedPayload. - Walkthrough
_settingsunwraps the options with a null guard (:28);IsConfiguredistrue(:31). The twoUploadAsyncoverloads carry the sameRS0026suppressions as the Azure sibling (:34,:39).UploadAsync(..., options, ct)(:40-95) composes the URL with the client's resolved region (:46), builds aPutObjectRequestwithAutoCloseStream = falsebecause the caller owns the stream (:48-57), setsDisablePayloadSigningwhenSendsUnsignedPayloadis true (:59-62), and copies non-emptyContentDispositionandCacheControlheaders (:64-72). Success returns the composed URI (:76-77);AmazonServiceExceptionand transport failures both yieldFileStorage.UploadFailed(:79-94).DeleteAsync(:98-125) callsDeleteObjectAsync; the comment notes S3 delete is already idempotent (a missing key answers 204), which matches the contract (:102-106). Failures yieldFileStorage.DeleteFailed(:109-124).SendsUnsignedPayload(:137-139) is true only for an absolute HTTPSServiceUrl. 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) whenFileStorage:Bucketis set. Tested inMMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/Storage/S3FileStorageServiceTests.csandMMCA.Common/Tests/Hosting/MMCA.Common.Aws.Tests/AwsServiceCollectionExtensionsTests.cs. - Caveats / not-in-source: no host in
MMCA.ADC/SourcecallsAddS3FileStorage, 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
GoogleCredentialandITokenAccess(Google.Apis.Auth.OAuth2). - Concept: cross-reference
CloudSqlIamAuthenticationConfigurator, which introduces the GCP form of token-as-password database authentication (the AWS form isRdsIamAuthenticationConfigurator). - Walkthrough:
Scopeishttps://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 loadsGoogleCredential.GetApplicationDefaultAsyncand callsCreateScoped(Scope)(CloudSqlAccessTokenSource.cs:27-32), then returnsGetAccessTokenForRequestAsync(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
CloudSqlIamAuthenticationConfiguratorbinds itsGetTokenAsyncas the token delegate (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/CloudSql/CloudSqlIamAuthenticationConfigurator.cs:34-35); referenced byMMCA.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 siblingRdsIamAuthenticationSettingsit 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
AddCloudSqlIamAuthenticationofGcpServiceCollectionExtensions(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:94);SectionPathis quoted in the TLS error ofCloudSqlIamAuthenticationConfigurator(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
IXmlDecryptorthat reversesGcpKmsXmlEncryptor, decrypting a stored key element with the Cloud KMS key that element names (GcpKmsXmlDecryptor.cs:10-15). - Depends on:
GcpKmsXmlEncryptorfor the element-name constants; GoogleKeyManagementServiceClient,DecryptRequest,ByteString;IServiceProvider. - Concept: cross-reference
GcpKmsXmlEncryptorfor 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 anIServiceProviderconstructor (GcpKmsXmlDecryptor.cs:12-14). - Walkthrough:
_client(GcpKmsXmlDecryptor.cs:18) is resolved in theIServiceProviderconstructor withGetRequiredService<KeyManagementServiceClient>()(GcpKmsXmlDecryptor.cs:22-25).Decrypt(encryptedElement)(GcpKmsXmlDecryptor.cs:29) reads thekmsKeyNameandvaluechildren (GcpKmsXmlDecryptor.cs:33-34) and throwsInvalidOperationExceptionnaming both elements when either is blank (GcpKmsXmlDecryptor.cs:35-39). It then sends a synchronousDecryptRequestwith the named key and the base64-decoded ciphertext (GcpKmsXmlDecryptor.cs:41-45) and parses the UTF-8 plaintext back into anXElement(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
AddGoogleCloudDataProtectionregisters or one the host registered first (GcpKmsXmlDecryptor.cs:14-15); seeGcpDataProtectionBuilderExtensions. - Where it's used: named as the decryptor type in
GcpKmsXmlEncryptor.Encrypt(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpKmsXmlEncryptor.cs:68); exercised byMMCA.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
IXmlEncryptorthat 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); GoogleKeyManagementServiceClient,CryptoKeyName,EncryptRequest,ByteString; DataProtectionEncryptedXmlInfo. - Concept introduced, key-ring encryption at rest with a cloud KMS. DataProtection lets a host plug an
IXmlEncryptorbetween 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 needscloudkms.cryptoKeyVersions.useToDecrypton 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,kmsKeyNameandvalue(GcpKmsXmlEncryptor.cs:28,31,34). The internal constructor takes the client and aCryptoKeyName, null-checking both (GcpKmsXmlEncryptor.cs:41-45);KeyNameexposes the key (GcpKmsXmlEncryptor.cs:48).Encrypt(plaintextElement)(GcpKmsXmlEncryptor.cs:51) serializes the element withSaveOptions.DisableFormattingto UTF-8 (GcpKmsXmlEncryptor.cs:55), sends a synchronousEncryptRequest(GcpKmsXmlEncryptor.cs:56-60), builds<encryptedKey>with an explanatory comment, the key name and the base64 ciphertext (GcpKmsXmlEncryptor.cs:62-66), and returnsEncryptedXmlInfo(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
ProtectKeysWithGoogleCloudKmsinGcpDataProtectionBuilderExtensions(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpDataProtectionBuilderExtensions.cs:59); its constants are read byGcpKmsXmlDecryptor; exercised byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/DataProtection/GcpKmsXmlEncryptionTests.csandMMCA.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
BucketandObjectNameparsed from thegs://bucket/objectform ofDataProtection: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:
Schemeisgs://(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 throwsInvalidOperationExceptionnaming the configuration key and the bad value (GcsObjectLocation.cs:37-38).ToString()reassemblesgs://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
GoogleCloudDataProtectionExtensionsbefore anything is registered (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudDataProtectionExtensions.cs:90), passed throughPersistKeysToGoogleCloudStorage(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpDataProtectionBuilderExtensions.cs:34) intoGcsXmlRepository; exercised byMMCA.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
IXmlRepositorythat 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; GoogleStorageClient,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
IfGenerationMatchset 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:
MaxAttemptsis 10 (GcsXmlRepository.cs:34); the root element isrepository(GcsXmlRepository.cs:36);WriterSettingsemit UTF-8 without a byte order mark (GcsXmlRepository.cs:39). The constructor takes the client, the location and aretryDelaybackoff 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 toMaxAttempts(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 withIfGenerationMatch = generation(GcsXmlRepository.cs:84-88). A 412PreconditionFailedmeans another writer won, so it sleepsretryDelay * 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 privateRead()(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).IXmlRepositoryis 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 areAzureDataProtectionExtensionsandAwsDataProtectionExtensions. - Where it's used: constructed by
PersistKeysToGoogleCloudStorageinGcpDataProtectionBuilderExtensions(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/DataProtection/GcpDataProtectionBuilderExtensions.cs:38); exercised byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/DataProtection/GcsXmlRepositoryTests.csandMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/GoogleCloudDataProtectionExtensionsTests.cs. - Caveats / not-in-source: the
friendlyNameparameter 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
INpgsqlDataSourceConfiguratorthat puts every PostgreSQL data source on Cloud SQL IAM database authentication: the password becomes an OAuth2 access token for thesqlservice.loginscope, handed to Npgsql through a periodic password provider (CloudSqlIamAuthenticationConfigurator.cs:8-13). - Depends on:
INpgsqlDataSourceConfigurator,ConnectionStringSettings,DataSourcesSettings,CloudSqlIamAuthenticationSettings,CloudSqlAccessTokenSource; NpgsqlNpgsqlDataSourceBuilder.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:
TokenRefreshIntervalis one minute (CloudSqlIamAuthenticationConfigurator.cs:26) andFailureRetryInterval30 seconds (CloudSqlIamAuthenticationConfigurator.cs:29). The public constructor chains tonew CloudSqlAccessTokenSource().GetTokenAsync(CloudSqlIamAuthenticationConfigurator.cs:34-35); the internal one accepts anyFunc<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 installsUsePeriodicPasswordProvider(GetPasswordAsync, TokenRefreshInterval, FailureRetryInterval)(CloudSqlIamAuthenticationConfigurator.cs:54).EnsureConfiguredConnectionStringsAreEncrypted(configuration)(CloudSqlIamAuthenticationConfigurator.cs:65) collects the top-levelConnectionStrings:PostgreSQLConnectionStringand everyDataSources:{name}:PostgreSQLConnectionString(CloudSqlIamAuthenticationConfigurator.cs:69-74) and checks each non-blank one parsed throughNpgsqlDataSourceBuilder, the same parserConfiguresees (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) acceptsRequire,VerifyCAorVerifyFull(CloudSqlIamAuthenticationConfigurator.cs:100) and otherwise throws a message quotingCloudSql: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), whileConfigurestill catches one supplied later (ADR-070). See ADR-061, ADR-113 and ADR-134. - Where it's used: registered by
AddCloudSqlIamAuthenticationinGcpServiceCollectionExtensions(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:102,104); exercised byMMCA.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-authnflag 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
IDataProtectionBuilderextensions that wire the GCP key ring:PersistKeysToGoogleCloudStorage(repository) andProtectKeysWithGoogleCloudKms(encryptor). - Depends on:
GcsXmlRepository,GcsObjectLocation,GcpKmsXmlEncryptor,GcsStorageClientFactory,GcsFileStorageSettings; externalsKeyManagementOptions,ConfigureOptions<T>, GoogleStorageClient,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:
RetryDelayis 50 ms, the backoff unit handed to the repository (GcpDataProtectionBuilderExtensions.cs:21).PersistKeysToGoogleCloudStorage(location)(GcpDataProtectionBuilderExtensions.cs:34) setsoptions.XmlRepositoryto a newGcsXmlRepositoryover the host's registeredStorageClient, or elseGcsStorageClientFactory.Create(new GcsFileStorageSettings())(GcpDataProtectionBuilderExtensions.cs:36-41).ProtectKeysWithGoogleCloudKms(keyName)(GcpDataProtectionBuilderExtensions.cs:54)TryAddSingletons a defaultKeyManagementServiceClient.Create()(GcpDataProtectionBuilderExtensions.cs:56) and setsoptions.XmlEncryptorto aGcpKmsXmlEncryptorover 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
TryAddin a laterAddGoogleCloudStorageand silently ignore itsFileStorage:GcsServiceUrl(GcpDataProtectionBuilderExtensions.cs:24-29). The KMS client, by contrast, must be in the container, because DataProtection activatesGcpKmsXmlDecryptorper 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), wherePushChannelis the FCM registration token. - Depends on: nothing first-party beyond the
UserIdentifierTypealias (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 ofSnsInstallation, 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 withstring.Create(CultureInfo.InvariantCulture, ...); that string is what theUserIdfield 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 byFcmPushDeviceRegistrar(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmPushDeviceRegistrar.cs:112), grouped by token inFcmNativePushSender(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:52-54), and the element type of everyIFcmInstallationRegistrymember.
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 keyNativePush:Enabled(FcmNativePushSettings.cs:18). - Depends on: nothing first-party; BCL
Regexsource generation (GeneratedRegex). - Concept: cross-reference
GcpServiceCollectionExtensions(AddFirebaseCloudMessagingPush) for the gate-plus-completeness registration pattern this class feeds; the AWS sibling isSnsNativePushSettings.[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) throwsInvalidOperationExceptionnamingNativePush:Fcm:BroadcastTopicwhen the topic failsTopicPattern, 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:
AddFirebaseCloudMessagingPushreads the gate and section, callsEnsureValidTopic, and binds the options (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:146-158);FcmPushClientFactorytakes it directly;FirestoreInstallationRegistry,FcmNativePushSenderandFcmPushDeviceRegistrarread it throughIOptions<FcmNativePushSettings>. Covered byMMCA.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 FirebaseAdminMessagingErrorCode, 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
IFcmMessagingClientwithout 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 forMessagingErrorCode.Unregistered, or forInvalidArgumentonly 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 withInvalidArgumentand a message naming the registration token, but any otherInvalidArgumentis 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 doublesFakeFcmMessagingClientandRecordingFcmMessagingClient; consumed byFcmNativePushSender(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
ConfigurationProviderthat loads every Google Secret Manager secret under one id prefix intoIConfiguration, mappingadc-prod-ConnectionStrings--DefaulttoConnectionStrings: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
AwsSecretsManagerConfigurationProviderand of the Key Vault provider used byAzureKeyVaultConfigurationExtensions: 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 = "--"andLatestVersion = "latest"(GoogleSecretManagerConfigurationProvider.cs:35-36); state is the client, project, prefix, optional interval, a disposalCancellationTokenSource, aLocktimer 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) returnsnullunless 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) setsData = Read()and, with an interval, creates a one-shotTimeronce under the gate (GoogleSecretManagerConfigurationProvider.cs:89-98).ReloadOnce(GoogleSecretManagerConfigurationProvider.cs:119-147) re-reads, swapsDataand callsOnReload()only whenHasSameDatais false (GoogleSecretManagerConfigurationProvider.cs:125-129), records any non-cancellation exception inLastReloadFailure(GoogleSecretManagerConfigurationProvider.cs:65,131-136), and re-arms the timer infinallyunless disposed (GoogleSecretManagerConfigurationProvider.cs:139-145).Read()(GoogleSecretManagerConfigurationProvider.cs:149-186) lists secrets with the filtername:"{prefix}"(GoogleSecretManagerConfigurationProvider.cs:156), keeps only idsKeyForaccepts (GoogleSecretManagerConfigurationProvider.cs:164), wraps a listingRpcExceptionin anInvalidOperationExceptionnamingsecretmanager.secrets.list(GoogleSecretManagerConfigurationProvider.cs:170-175), then reads each value into a case-insensitive dictionary.ReadLatest(GoogleSecretManagerConfigurationProvider.cs:188-218) gets thelatestversion, returnsnullonNotFound(GoogleSecretManagerConfigurationProvider.cs:197-201) or a non-Enabledstate (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 thelatestalias 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 becauseConfigurationProvider.Loadis 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 byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Secrets/GoogleSecretManagerConfigurationProviderTests.cs. - Caveats / not-in-source:
LastReloadFailureis 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
IConfigurationSourcethat carries a client factory, the project id, the name prefix and the reload interval, and builds aGoogleSecretManagerConfigurationProvider. - Depends on:
GoogleSecretManagerConfigurationProvider; GoogleSecretManagerServiceClient,IConfigurationSource. - Concept: the standard configuration source/provider split; cross-reference
AwsSecretsManagerConfigurationSourcefor the identical AWS shape. - Walkthrough: primary-constructor parameters
clientFactory,projectId,namePrefix,reloadInterval(GoogleSecretManagerConfigurationSource.cs:15-19), withProjectId,NamePrefixandReloadIntervalexposed 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 toBuild, and the internal properties let tests assert what the extension method configured without a live client. - Where it's used: added by
AddGoogleSecretManagerConfigurationwithSecretManagerServiceClient.Createas the factory (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleSecretManagerConfigurationExtensions.cs:118-119), seeGoogleSecretManagerConfigurationExtensions; asserted byMMCA.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
FcmNativePushSenderandFcmPushDeviceRegistrarcan be unit-tested againstFakeFcmInstallationRegistrywhile 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 coreIPushDeviceRegistrar. - Walkthrough:
GetAsync(IFcmInstallationRegistry.cs:15) returnsnullfor 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 withTryAddSingleton(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:161); consumed byFcmNativePushSenderandFcmPushDeviceRegistrar.
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; FirebaseAdminMessage. - Concept: cross-reference
IFcmInstallationRegistryfor the internal-port rationale. FirebaseAdmin'sFirebaseMessagingis a sealed singleton bound to a process-global app, so the interface is what lets tests substituteFakeFcmMessagingClientorRecordingFcmMessagingClient. - Walkthrough:
MaxMessagesPerSend = 500(IFcmMessagingClient.cs:17), FirebaseAdmin's own limit onSendEachAsync(IFcmMessagingClient.cs:13-16).SendEachAsync(IFcmMessagingClient.cs:23) returns one outcome per message, in order.SendAsync(IFcmMessagingClient.cs:29) throws on failure.SubscribeToTopicAsyncandUnsubscribeFromTopicAsync(IFcmMessagingClient.cs:36,43) take one token and return FCM's refusal reason, ornullon 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 aTryAddSingletonfactory (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:160); consumed byFcmNativePushSenderandFcmPushDeviceRegistrar.
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 namedFirebaseAppover Application Default Credentials and delegates toFirebaseMessaging. - Depends on:
IFcmMessagingClient,FcmSendOutcome; FirebaseAdminFirebaseApp,FirebaseMessaging,TopicManagementResponse; GoogleGoogleCredential. - 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 aSemaphoreSlimguards a name that FirebaseAdmin lets be created only once. - Walkthrough: fields
_gate(aSemaphoreSlim(1, 1)),_app,_messaging(FirebaseMessagingClient.cs:19-21);AppNameisMMCA.Common.Gcp.Fcm.{Guid:N}, unique per instance (FirebaseMessagingClient.cs:24).SendEachAsync(FirebaseMessagingClient.cs:27-34) maps each batch response toFcmSendOutcome.Successor an outcome carryingMessagingErrorCodeand 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 throughFirstError(FirebaseMessagingClient.cs:66-74), which returnsnullwhenFailureCount == 0, else the first error'sReasonor"unknown-error".GetMessagingAsync(FirebaseMessagingClient.cs:76-100) returns the cached instance, otherwise waits on the gate, re-checks, fetchesGoogleCredential.GetApplicationDefaultAsync, and callsFirebaseApp.Create(..., AppName)andFirebaseMessaging.GetMessaging(FirebaseMessagingClient.cs:89-91).Disposedeletes 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), seeFcmPushClientFactory; exercised inMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/AddFirebaseCloudMessagingPushTests.cs. - Caveats / not-in-source: the fast path reads
_messagingwithout the gate (FirebaseMessagingClient.cs:78-81); a call racingDisposeis 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; GoogleFirestoreDb,CollectionReference,DocumentSnapshot,FieldValue. - Concept: ownership rules as a read-decide-write transaction. The AWS sibling
SnsInstallationRegistryexpresses the same rule as a DynamoDB condition expression; Firestore has no conditional put, so the rule runs insideRunTransactionAsync, 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 oneWhereInfilter;UserIdequality is served by the automatic single-field index, so no composite index is needed (FirestoreInstallationRegistry.cs:19-22). Field namesUserId,Platform,PushChannel,UpdatedAt(FirestoreInstallationRegistry.cs:25-28);Collectionresolves 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 oneWhereInquery per chunk (FirestoreInstallationRegistry.cs:74-76).DeleteOwnedAsyncandDeleteIfPushChannelAsync(FirestoreInstallationRegistry.cs:90-95) both go throughDeleteWhenAsync(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) returnsnullfor a missing document or one lacking a non-emptyUserIdorPushChannel, and defaults a missingPlatformto 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); itsMaxUsersPerQueryconstant is also read byFcmNativePushSender(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:39); covered against the emulator byMMCA.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
FirestoreDband theFirebaseMessagingClientthe FCM push adapter needs, from oneFcmNativePushSettings. - Depends on:
FcmNativePushSettings,FirebaseMessagingClient; GoogleFirestoreDbBuilder,EmulatorDetection. - Concept: the same factory shape as
SnsPushClientFactoryandGcsStorageClientFactory: 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 withEmulatorDetection = EmulatorDetection.None(FcmPushClientFactory.cs:29).CreateFirestore(FcmPushClientFactory.cs:36) builds it.CreateMessaging(FcmPushClientFactory.cs:41-46) returnsnew FirebaseMessagingClient(settings.ProjectId!). - Why it's built this way: ignoring
FIRESTORE_EMULATOR_HOSTmakes the endpoint a matter of configuration alone, the same posture as the Cloud Storage adapter; an emulator run registers its ownFirestoreDbfirst (FcmPushClientFactory.cs:13-19). - Where it's used: the
TryAddSingletonfactories inAddFirebaseCloudMessagingPush(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:159-160), so a host-registeredFirestoreDborIFcmMessagingClientwins; asserted byMMCA.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; FirebaseAdminFirebaseException,Grpc.Core.RpcException,LoggerMessagesource generation. - Concept: cross-reference
FirestoreInstallationRegistryfor the ownership rule andSnsPushDeviceRegistrarfor the AWS twin. Errors come back asResultfailures, 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:
TopicreadsBroadcastTopicfrom options (FcmPushDeviceRegistrar.cs:32).UpsertAsync(FcmPushDeviceRegistrar.cs:35-65) upper-cases the platform (FcmPushDeviceRegistrar.cs:39);APNSgets aPushDevice.UnsupportedPlatformvalidation failure explaining that iOS clients must register an FCM token (FcmPushDeviceRegistrar.cs:40-46), and anything other thanFCMV1gets the same code (FcmPushDeviceRegistrar.cs:48-54);RpcExceptionorFirebaseExceptionmaps toPushDevice.UpsertFailed(FcmPushDeviceRegistrar.cs:60-64,104-108).RegisterAsync(FcmPushDeviceRegistrar.cs:110-142) builds the installation with the normalizedFcmV1Platform(FcmPushDeviceRegistrar.cs:112), callsTryClaimAsync(FcmPushDeviceRegistrar.cs:116) and returnsPushDevice.InstallationConflictwhen 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 aFirebaseExceptionas 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 toPushDevice.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
IPushDeviceRegistrarbyAddFirebaseCloudMessagingPush(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:163), replacingNullPushDeviceRegistrar; covered byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/FcmPushDeviceRegistrarTests.cs. - Caveats / not-in-source:
UnsubscribeReplacedAsynccatches onlyFirebaseException(FcmPushDeviceRegistrar.cs:155); any other exception from that call propagates toUpsertAsync, where anRpcExceptionwould still map toUpsertFailedeven 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
Messagefor 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 privateBuild, so a targeted send and a broadcast cannot drift in payload shape. UnlikeSnsPushMessage, it does not reuse the core payload builders, because FirebaseAdmin takes a typedMessagerather than raw JSON. - Walkthrough:
ApsKey = "aps"(FcmPushMessage.cs:19).ForToken(FcmPushMessage.cs:27-41) builds and setsmessage.Tokeninside aCS0618suppression (FcmPushMessage.cs:37-39).ForTopic(FcmPushMessage.cs:49-54) builds and setsTopic.Build(FcmPushMessage.cs:56-76) null-checks title and body, setsNotification(FcmPushMessage.cs:64), copies metadata intoDataonly when non-empty (FcmPushMessage.cs:65), and sets anApnsConfigwith anApsAlertplus the metadata asCustomDataminus anyapskey (FcmPushMessage.cs:66-74). - Why it's built this way: the obsolete
Message.Tokenis 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). Filteringapsout ofCustomDatakeeps metadata from clobbering the alert block (FcmPushMessage.cs:18). See ADR-044 and ADR-134. - Where it's used:
FcmNativePushSenderfor targeted sends and the broadcast (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/Notifications/FcmNativePushSender.cs:59,95); asserted inMMCA.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 itsMaxUsersPerQueryconstant),FcmNativePushSettings; FirebaseAdminFirebaseException,Grpc.Core.RpcException,LoggerMessagesource generation. - Concept: cross-reference
SnsNativePushSenderfor 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 withFcmInstallation.UserKey, de-duplicates, and chunks byFirestoreInstallationRegistry.MaxUsersPerQuery(FcmNativePushSender.cs:39), logging and skipping a batch whose query throwsRpcException(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 ofIFcmMessagingClient.MaxMessagesPerSend(FcmNativePushSender.cs:57-63), logging and continuing onFirebaseException(FcmNativePushSender.cs:65-69). Per outcome it counts a success, prunes onIsStaleToken, or logs the error code (FcmNativePushSender.cs:71-86), and finally logs the delivered count (FcmNativePushSender.cs:89).BroadcastAsync(FcmNativePushSender.cs:93-98) sendsFcmPushMessage.ForTopictoBroadcastTopic.PruneAsync(FcmNativePushSender.cs:100-114) deletes every installation in the group with the token-conditionalDeleteIfPushChannelAsync, logging anRpcException. 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
INativePushSenderbyAddFirebaseCloudMessagingPush(MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GcpServiceCollectionExtensions.cs:162), replacingNullNativePushSender; covered byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Notifications/FcmNativePushSenderTests.cs. - Caveats / not-in-source:
BroadcastAsynchas 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 concreteFirestoreInstallationRegistryrather than fromIFcmInstallationRegistry(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
FileStoragesection 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
Urionly. - Concept: one shared
FileStoragesection, vendor-prefixed keys.SectionNameis"FileStorage"(:15), the same section AzureBlobFileStorageSettings and S3FileStorageSettings bind, but every key here carries aGcsprefix, 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 isinit-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 optionalx-goog-user-projectquota project, needed only with user-account credentials such asgcloud auth application-default login(:38-43).- Both URI properties state "Only an absolute URL counts" (
:24,:34); the consumers enforce that withIsAbsoluteUripatterns (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 byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsStorageClientFactoryTests.csandGcsObjectUrlTests.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 substituteFakeTelemetryTokenSource(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
GoogleCredentialandITokenAccessfrom Google.Apis.Auth. - Concept: lazy, retry-friendly credential load.
[Rubric §11, Security]assesses least privilege: the requested scope is the broadcloud-platform(:19), and the comment records why: the narrowertrace.appenddoes 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:
Scopeconstant (:19); a nullable_credentialcache (:21).GetTokenAsync(:24-37) loadsGoogleCredential.GetApplicationDefaultAsyncandCreateScoped(Scope)only when the cache is empty (:29-34), then returnsGetAccessTokenForRequestAsync(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
GetAccessTokenForRequestAsyncserves 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 AzurePersistKeysToAzureBlobStorage/ProtectKeysWithAzureKeyVaultextensions. - 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 whenDataProtection:BlobStorageUriis 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): oneDefaultAzureCredentialshared by both sinks so they share a token cache (:72-73), thenSetApplicationName(...).PersistKeysToAzureBlobStorage(new Uri(blobStorageUri), credential). - Gate 2 (
:86-90):ProtectKeysWithAzureKeyVaultonly whenDataProtection:KeyVaultKeyUriis set.
- Keys (
- Why it's built this way: the comment at
:79-85records 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:127andMMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:72. The tripwire message naming this call is atMMCA.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 throughIConfigurationand 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 isDefaultAzureCredential(managed identity when deployed), needing Key Vault Secrets User on the vault (:52-56). - Walkthrough:
- Keys (
:35-41):KeyVault:Uri(the gate) and optionalKeyVault:ReloadIntervalMinutes; unset means the vault is read once at startup. - Naming (
:45-49): a double dash maps onto the configuration separator, soConnectionStrings--Defaultarrives asConnectionStrings:Default. - Receipt then gate (
:84-94): the marker is registered first, then a blank URI returns. - Reload interval (
:102-113): parsed withInvariantCulture; a non-positive or non-integer value throwsInvalidOperationException(: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 asUriFormatException(documented at:74-76).
- Keys (
- 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). BecauseConfigurationManagerloads each source as it is added, the vault is read synchronously at this call, so it belongs early inProgram.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 serviceProgram.csfiles (for exampleMMCA.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 whenAPPLICATIONINSIGHTS_CONNECTION_STRINGis 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:
ConnectionStringKeyis the public constant"APPLICATIONINSIGHTS_CONNECTION_STRING"(:18). The method first scans the service collection for a non-keyedHostIntegrationMarkernamedServiceDefaultsTelemetryand throwsInvalidOperationExceptionwhen 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 callsUseAzureMonitor()(: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 byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsObjectUrlTests.cs. - Walkthrough:
PublicEndpointishttps://storage.googleapis.com/(:12).Compose(:28-42) null-guards both arguments, escapes the name, then picks in order (:15-21): an absoluteGcsPublicBaseUrlyields{base}/{object}with no bucket (:35-38); otherwise an absoluteGcsServiceUrlor the public endpoint yields{endpoint}/{bucket}/{object}(:40-41).EscapeNamesplits on/, appliesUri.EscapeDataStringto each segment and rejoins (:44-45), so spaces and non-ASCII names still produce a valid URL while keeping the separators.TrimEndstrips 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
StorageClientBuilderand builds theStorageClient(GcsStorageClientFactory.cs:23,:46). - Depends on: GcsFileStorageSettings; externals
StorageClientBuilder,StorageClient(Google.Cloud.Storage.V1). - Concept: the builder/build split, the same shape as S3StorageClientFactory.
BuildBuilderreturns 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 absoluteGcsServiceUrlsetsBaseUrito{GcsServiceUrl}/storage/v1/(:31) and setsUnauthenticatedAccesswhen the scheme ishttp(:32). A non-blankGcsProjectIdsetsQuotaProject(:35-38).CreateisBuildBuilder(settings).Build()(:46). - Why it's built this way: the doc comment states
STORAGE_EMULATOR_HOSTis 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; externalsCryptoKeyName(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.ParseandCryptoKeyName.TryParseboth fail with the offending key named before anything is registered. - Registration (
:102-114): receipt, application name,PersistKeysToGoogleCloudStorage(location), thenProtectKeysWithGoogleCloudKmsonly when a key name parsed.
- Keys (
- 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 withDataProtection:BlobStorageUriorDataProtection:SsmParameterPrefixfails 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/Sourceor elsewhere inMMCA.Common/Source; the tripwire message naming it is atMMCA.Common/Source/Hosting/MMCA.Common.Aspire/Integrations/HostIntegrationTripwire.cs:96. - Caveats / not-in-source: the
StorageClientreuse 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
IHostedServicethat 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, OpenTelemetrySuppressInstrumentationScope. - 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 insideSuppressInstrumentationScope.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):RefreshInterval1 minute,FailureRetryInterval30 seconds. - State (
:38-43): a stoppingCancellationTokenSource, avolatile string? _currentexposed asCurrent, 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 logsLogRefreshFailedand returnsfalse(: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-generatedWarningstating traces and logs are not exported until a refresh succeeds.
- Intervals (
- Why it's built this way: the export path reads a held string instead of awaiting a credential, which also serves the synchronous
Sendoverride 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 byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Telemetry/GoogleCloudTelemetryAuthHandlerTests.cs. - Caveats / not-in-source:
StartAsyncawaits 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
DelegatingHandlerthat stamps the bearer token and thex-goog-user-projectquota-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 throwsHttpRequestExceptionwithHttpStatusCode.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:
QuotaProjectHeaderconstant (:18). BothSend(:22-26) andSendAsync(:30-34) callAuthorizeand delegate.Authorize(:41-57) readsaccessToken.Current, setsAuthorization: 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
Sendtoo covers OTLP exporter paths that send synchronously; both paths share oneAuthorize. Telemetry rationale: ADR-041. - Where it's used: constructed per exporter
HttpClientin GoogleCloudTelemetryExporterExtensions (MMCA.Common/Source/Hosting/MMCA.Common.Gcp/GoogleCloudTelemetryExporterExtensions.cs:203-207). Tested inMMCA.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 throughIConfiguration(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--Defaultarrives asConnectionStrings:Default(:48-55). Thelatestversion is read; a disabled, destroyed or version-less secret is skipped (:53-55). - Walkthrough:
SectionPathis"GoogleSecretManager"(:25). Keys (:37-44):ProjectIdandNamePrefixtogether are the gate,ReloadIntervalMinutesoptional. 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) andconfiguration.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 atMMCA.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 whenGoogleCloudTelemetry:ProjectIdis 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 withsend_batch_max_size: 200instead (:78-86). - Walkthrough:
- Constants (
:23-38):ProjectIdKey,EndpointKey,DefaultEndpoint(https://telemetry.googleapis.com/), andProjectIdAttribute=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,TryAddSingletonof the token source, the access-token singleton and its hosted-service registration. - Pipeline (
:137-144): adds thegcp.project_idresource attribute and hand-built batch processors forv1/tracesandv1/logs. The comment at:133-136explains why notAddOtlpExporter: the SDK refuses a signal-specificAddOtlpExporterwhereUseOtlpExporter(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 sonew Uri(root, "v1/traces")does not replace the last segment (:172-174).SignalEndpointis thatUricombine (:181).CreateExporterOptions(:193-209): full endpoint URL,HttpProtobuf,Headers = null(:200) so anOTEL_EXPORTER_OTLP_HEADERSmeant for the vendor-neutral exporter never reaches Google (:188-191), and anHttpClientFactorywrapping GoogleCloudTelemetryAuthHandler with the exporter's timeout (:203-207).
- Constants (
- 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_taskversusgeneric_node) is left toOTEL_SERVICE_NAMEandOTEL_RESOURCE_ATTRIBUTES(:70-75). Recorded in ADR-041 and ADR-134. - Where it's used: no call site in
MMCA.ADC/Source; tripwire message atMMCA.Common/Source/Hosting/MMCA.Common.Aspire/Integrations/HostIntegrationTripwire.cs:101. Tested byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Telemetry/GoogleCloudTelemetryExporterExtensionsTests.csandGoogleCloudTelemetryExportTests.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 toResultwith stable codesFileStorage.UploadFailed/FileStorage.DeleteFailed, not exceptions (see the Result pattern). - Walkthrough:
IsConfiguredistrue(:33). The shortUploadAsyncoverload forwards withFileUploadOptions.None(:37-38).UploadAsync(:42-84): composes the URL first so a configuration that cannot name the object leaves no orphan (:46-48); builds aStorageObjectwhoseContentDispositionandCacheControlare set only when non-empty, stored as object metadata (:50-59); uploads without closing the caller's stream (:63-65).GoogleApiExceptionand transport failures log and returnUploadFailed(:68-83).DeleteAsync(:87-116): a404GoogleApiExceptionis success (:94-99), mirroringDeleteBlobIfExists; other API and transport failures returnDeleteFailed.IsTransportFailure(:124-126):HttpRequestException,IOException, or anOperationCanceledExceptionthe 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 byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/Storage/GcsFileStorageServiceTests.cs; also referenced byCloudVendorRulesTests.csin 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, andAddAzureServiceBusTransport(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 whenContainerNameis blank (:49-52) or when there is neither an absoluteServiceUrinor a connection string (:54-59; an empty-stringServiceUribinds as a relativeUri,:54). Registers aBlobContainerClientwithTryAddSingleton, preferringServiceUri+DefaultAzureCredentialover the connection string (:61-67), then the transient service (:68).AddAzureNotificationHubsPush(:83-105): binds options, requiresEnabled,ConnectionStringandHubName(:91-96), registersINotificationHubClientviaTryAddSingletonand 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 BEFOREAddBrokerMessaging, which selects the transport byMessageBus: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
TryAddclient registration lets a host supply its own client first. - Where it's used:
MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:261andMMCA.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 exampleMMCA.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, andAddFirebaseCloudMessagingPush(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): bindsFileStorage(:52-53) and returns whenGcsBucketis blank (:56-59), even if the section carries the S3Bucketor AzureContainerName(:38-39). Registers the client throughTryAddSingleton(_ => GcsStorageClientFactory.Create(settings))(:61), so a host-registeredStorageClientwins, and the transient service (:62).AddCloudSqlIamAuthentication(:90-105): gated onCloudSql:IamAuthentication:Enabled(:94-98); callsEnsureConfiguredConnectionStringsAreEncryptedso a non-TLS PostgreSQL connection string fails at registration (:100-102), thenAddNpgsqlDataSourceConfigurator(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-authnproxy alternative (:67-84); SQL Server, SQLite and Cosmos are untouched.AddFirebaseCloudMessagingPush(:142-166): requiresNativePush:Enabledtrue and a completeNativePush:Fcmsection (: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 withPushDevice.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 byMMCA.Common/Tests/Hosting/MMCA.Common.Gcp.Tests/GcpServiceCollectionExtensionsTests.cs.
⬅ Common AI Integration • Index • Testing & Quality Infrastructure ➡