AverAzure — Session Context¶
What we built¶
A single .NET 8 Web API project called AverAzure that demonstrates:
- Azure Blob Storage via
IDocumentStoreabstraction - RabbitMQ messaging via a custom wrapper
- Structured logging via Serilog → Seq
- Scalar UI (Swashbuckle generates spec, Scalar renders UI)
Project structure¶
AverAzure/
├── Abstractions/
│ ├── IDocumentStore.cs
│ ├── IInvoiceStore.cs
│ ├── IImageStore.cs
│ ├── IMessageBus.cs
│ └── MessagingContracts.cs # IIntegrationEvent, ICommand, base records
│
├── Messaging/
│ ├── Events/
│ │ └── InvoiceUploadedEvent.cs
│ ├── MessageEnvelope.cs
│ ├── RabbitMqConnection.cs
│ ├── RabbitMqConsumer.cs # Base BackgroundService with retry + DLQ
│ └── RabbitMqMessageBus.cs
│
├── Consumers/
│ └── InvoiceUploadedConsumer.cs # Handler + Consumer for InvoiceUploadedEvent
│
├── Storage/
│ ├── DocumentStoreOptions.cs
│ ├── DocumentStoreFactory.cs
│ ├── AzureBlobDocumentStore.cs
│ ├── InvoiceStore.cs # Wraps IDocumentStore for invoices container
│ └── ImageStore.cs # Wraps IDocumentStore for originals/thumbnails/web
│
├── Extensions/
│ ├── ConfigurationBuilderExtensions.cs # AddSecretsProvider()
│ └── ServiceCollectionExtensions.cs # AddDocumentStores(), AddMessaging()
│
├── Controllers/
│ ├── InvoicesController.cs
│ └── ImagesController.cs
│
├── Program.cs
├── appsettings.json
├── appsettings.Development.json
├── Dockerfile
├── docker-compose.yml
├── .env.example
└── .gitignore
API endpoints¶
GET /healthPOST /api/invoices— upload file toinvoicescontainer, publishes InvoiceUploadedEventGET /api/invoices— list all invoicesGET /api/invoices/{name}— download invoice by namePOST /api/images— upload tooriginalscontainer, Azure Function processes itGET /api/images— list all originalsGET /api/images/{name}/thumbnail— fetch 150x150 thumbnail (generated by Azure Function)GET /api/images/{name}/web— fetch 800x800 web version (generated by Azure Function)GET /scalar/v1— Scalar UIGET /swagger/v1/swagger.json— OpenAPI spec
RabbitMQ topology¶
- Exchange:
domain.events(Topic) — for events - Exchange:
domain.commands(Direct) — for commands - Queue:
aver.invoice-uploaded.queue— bound todomain.events, routing keyInvoiceUploadedEvent - Retry queue:
aver.invoice-uploaded.queue.retry— 30s TTL, dead letters back to main queue - DLQ:
aver.invoice-uploaded.queue.dlq— after 3 failed attempts
Event flow on invoice upload¶
POST /api/invoices— file uploaded to Azure Blob Storage (invoicescontainer)InvoiceUploadedEventpublished todomain.eventsexchangeInvoiceUploadedConsumerreceives it fromaver.invoice-uploaded.queueInvoiceUploadedHandlerlogs structured fields (InvoiceName, FileSizeBytes, UploadedAt)- Message ACKed and removed from queue
- All log entries visible in Seq at
http://localhost:8081
Image flow¶
POST /api/images— file uploaded to Azure Blob Storage (originalscontainer)- Azure Function (
image-processor) triggered via Blob trigger onoriginals/{name} - Function generates 150x150 thumbnail →
thumbnailscontainer - Function generates 800x800 web version →
webcontainer GET /api/images/{name}/thumbnailfetches fromthumbnailscontainerGET /api/images/{name}/webfetches fromwebcontainer
Storage abstraction¶
IDocumentStore— low level: UploadAsync, DownloadAsync, DeleteAsync, ListAsyncIInvoiceStore— wraps IDocumentStore, intent-named methods for invoices containerIImageStore— wraps three IDocumentStore instances (originals, thumbnails, web)DocumentStoreFactory— switches onProviderconfig value ("Azure", "S3", "Minio" stubs)AzureBlobDocumentStore— usesDefaultAzureCredential
Config structure (appsettings.json)¶
{
"Secrets": { "Provider": "None" },
"DocumentStore": {
"Invoices": { "Provider": "Azure", "AccountUrl": "...", "ContainerName": "invoices" },
"Images": {
"Originals": { "Provider": "Azure", "AccountUrl": "...", "ContainerName": "originals" },
"Thumbnails": { "Provider": "Azure", "AccountUrl": "...", "ContainerName": "thumbnails" },
"Web": { "Provider": "Azure", "AccountUrl": "...", "ContainerName": "web" }
}
},
"RabbitMq": { "ConnectionString": "amqp://guest:guest@rabbitmq:5672" },
"Seq": { "ServerUrl": "http://seq:5341" }
}
Credentials and identity¶
DefaultAzureCredentialused throughout — no connection strings in code- On Azure infra (VM, Function App) → Managed Identity resolves automatically
- In Docker containers (local or VPS) → Service Principal via env vars:
AZURE_TENANT_IDAZURE_CLIENT_IDAZURE_CLIENT_SECRET
- SP created with
Storage Blob Data Contributorrole scoped tolearn_week_1resource group - Key Vault wiring is in place (
AddSecretsProvider()) butSecrets:Provideris set to"None"for now — switching to"AzureKeyVault"is a single config change
Docker compose stack¶
Three services:
api— .NET 8 API, port8080:8080, depends on rabbitmq healthcheckrabbitmq—rabbitmq:3.13-management, port15672:15672(management UI only, 5672 internal only)seq—datalust/seq, ports5341:5341(ingestion) and8081:80(UI)
Named volumes for RabbitMQ and Seq — required for Podman rootless permission handling. SEQ_FIRSTRUN_NOAUTHENTICATION: true — skips password requirement for local dev.
Azure resources in use¶
- Subscription:
b089d18c-cba7-4719-af2f-ff9ab3f8b56e - Resource Group:
learn_week_1 - Storage Account:
averblobstore(West India)- Containers:
invoices,originals,thumbnails,web
- Containers:
- Key Vault:
peppol-vault(West India) — not active yet, wiring in place - Function App:
image-processor(East US, Consumption Windows)- Blob trigger on
originals/{name} - Managed Identity:
aa7657b1-b467-4c45-8c4f-7684396b9bd1 - Roles: Storage Blob Data Contributor, Queue Data Contributor, Table Data Contributor on
averblobstore
- Blob trigger on
Proved working¶
- Local (Podman on Fedora) — all containers up, full invoice flow, image upload and thumbnail fetch
- VPS (real Docker alongside Coolify) — cloned via SSH key, full flow proved via curl
What's next¶
docker compose downon VPSdocker swarm init- Convert
docker-compose.ymltodocker-stack.yml docker stack deploy- Then CI/CD + interview prep