ABP series · 2026-02-23
ABP + Dapr: Service Invocation and Pub/Sub in Production
Integrating Dapr service invocation and pub/sub with ABP Framework modules — what actually composes cleanly, what to wrap, and the event-driven pattern that stays tenant-safe.
ABP gives you modules, unit of work and distributed events. Dapr gives you service-to-service calls and pub/sub without the SDK weight. Together they cover most of what a microservice platform needs — if you place the boundary correctly.
What composes cleanly
Dapr’s service invocation (dapr invoke) fits naturally behind ABP’s IHttpClientFactory-style patterns: define a typed client, call the sidecar over HTTP, and let ABP’s exception handling map non-2xx responses into your domain exceptions.
Pub/sub maps onto ABP’s IDistributedEventBus. You keep raising domain events inside your handlers — ABP fires them locally. A thin publisher forwards them to the Dapr pub/sub component when an outbox flag says “cross-service”.
What needs a wrapper
The Dapr sidecar’s dapr invoke responses have no tenant context by default. When service A calls service B, the tenant id must travel explicitly — a header like X-Tenant-Id that your ABP middleware on the receiving side reads and feeds into CurrentTenant.Change. Do this in one IHttpClientFactory delegate handler on the caller side, and one middleware on the receiver side. Nowhere else.
// caller side — one handler, every outgoing call
public class TenantHeaderHandler : DelegatingHandler
{
private readonly ICurrentTenant _currentTenant;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage req, CancellationToken ct)
{
if (_currentTenant.Id.HasValue)
req.Headers.TryAddWithoutValidation("X-Tenant-Id", _currentTenant.Id.Value.ToString());
return base.SendAsync(req, ct);
}
}
The event shape that stays tenant-safe
Include tenant id in every published event envelope — not in the payload by convention, but as a required field. Consumers resolve it before touching the database:
using (CurrentTenant.Change(e.TenantId))
{
await _orderRepository.UpdateAsync(...);
}
A consumer that forgets this line silently writes to the wrong tenant. A required TenantId field and a single integration test that asserts cross-tenant isolation closes that class of bug. ABP modules stay modular; Dapr stays transport; you keep the domain.