The third part of the series about Azure Container Apps. In the second part we configured scaling rules. Here we see how services communicate with each other using Dapr.
What is Dapr and what problem does it solve
Dapr (Distributed Application Runtime) is a runtime that provides building blocks for distributed applications: service invocation, pub/sub, state management, secrets, bindings. The central idea: instead of writing code specific to each technology (Service Bus SDK, Cosmos DB SDK, HTTP client with retry, etc.), you interact with a uniform API, and Dapr translates to the actual infrastructure.
The concrete benefit in microservices: you separate what the application does from how it communicates with the infrastructure. You change the message broker from Service Bus to RabbitMQ by modifying a configuration file, not the code.
In Container Apps, Dapr is built-in — you don't install anything, you just enable it. ACA automatically runs the Dapr sidecar alongside your container.
Enabling Dapr
az containerapp create \
--name order-service \
--resource-group my-rg \
--environment my-aca-env \
--image myregistry.azurecr.io/order-service:latest \
--target-port 8080 \
--ingress internal \
--enable-dapr \
--dapr-app-id order-service \
--dapr-app-port 8080
dapr-app-id is the identifier through which other services address this service. dapr-app-port is the port on which Dapr communicates with your application. In .NET, you add the Dapr SDK:
dotnet add package Dapr.Client
dotnet add package Dapr.AspNetCore
Service invocation — calls between services
Without Dapr, to call another service you need its URL, a configured HttpClient, a retry policy, mTLS. With Dapr, you address the service by app-id and Dapr takes care of the rest (service discovery, retry, mTLS between sidecars).
// Program.cs
builder.Services.AddDaprClient();
// Call to another service by app-id, not by URL
public class OrderService
{
private readonly DaprClient _dapr;
public OrderService(DaprClient dapr) => _dapr = dapr;
public async Task GetCustomerAsync(string customerId)
{
// Invoke "customer-service" -- Dapr resolves where it runs
return await _dapr.InvokeMethodAsync(
HttpMethod.Get,
appId: "customer-service",
methodName: $"customers/{customerId}");
}
}
The called service does not need anything special — it is a normal HTTP endpoint. Dapr injects the request through the sidecar.
Pub/Sub with Service Bus
Pub/sub is where Dapr shines. You configure a Dapr component that points to Service Bus, and the code publishes and consumes messages through a uniform API.
The Dapr component (pub/sub)
# pubsub-servicebus.yaml
componentType: pubsub.azure.servicebus.topics
version: v1
metadata:
- name: namespaceName
value: "my-servicebus.servicebus.windows.net"
- name: azureClientId
value: "" # authentication without secret
scopes:
- order-service
- notification-service
# Register the component in the environment
az containerapp env dapr-component set \
--name my-aca-env \
--resource-group my-rg \
--dapr-component-name orderpubsub \
--yaml pubsub-servicebus.yaml
Publishing
public async Task PublishOrderCreatedAsync(Order order)
{
await _dapr.PublishEventAsync(
pubsubName: "orderpubsub",
topicName: "order-created",
data: order);
}
Consuming (subscribe)
// Program.cs
app.MapSubscribeHandler(); // endpoint for registering Dapr subscriptions
// Controller that receives the events
[ApiController]
public class NotificationController : ControllerBase
{
[Topic("orderpubsub", "order-created")]
[HttpPost("order-created")]
public async Task OnOrderCreated(Order order)
{
await _emailService.SendConfirmationAsync(order);
return Ok();
}
}
The [Topic] attribute binds the method to the Dapr topic. When a message arrives on order-created, Dapr delivers it to this method. No Service Bus SDK code, no manual connection management.
State management with Cosmos DB
Dapr also offers an abstract key-value state store, with Cosmos DB (or Redis, or others) behind it.
# statestore-cosmos.yaml
componentType: state.azure.cosmosdb
version: v1
metadata:
- name: url
value: "https://my-cosmos.documents.azure.com:443/"
- name: database
value: "StateDb"
- name: collection
value: "state"
- name: azureClientId
value: ""
scopes:
- order-service
// Save and read state through a uniform API
public async Task SaveCartAsync(string userId, Cart cart)
{
await _dapr.SaveStateAsync("statestore", $"cart-{userId}", cart);
}
public async Task GetCartAsync(string userId)
{
return await _dapr.GetStateAsync("statestore", $"cart-{userId}");
}
Note again the azureClientId in all components: Dapr authenticates to Service Bus and Cosmos DB through Managed Identity, without any connection string. The security foundation from previous articles applies fully.
When to use Dapr and when not
Dapr adds real value to a microservices architecture with inter-service communication, pub/sub, and portability needs. For a single monolithic service, it is an unjustified overhead — native Azure SDKs are sufficient. As always in the series: you adopt complexity only when it solves a concrete problem.
What’s next
In the fourth and final part we put everything together with a complete CI/CD pipeline: GitHub Actions that builds the image, pushes it to Azure Container Registry, and deploys it to Container Apps — all with secretless authentication via OIDC.
Questions? Write to me at contact@ludoprogramming.com.