How Do You Publish and Consume Events in ASP.NET Core?
In ASP.NET Core microservices, events are commonly published and consumed through a message broker such as:
- Azure Service Bus
- RabbitMQ
- Apache Kafka
I'll use Azure Service Bus Topic + Subscriptions because it clearly demonstrates the Publish/Subscribe model we discussed.
The architecture will be:
Order Service
│
│ Publish
▼
OrderCreatedIntegrationEvent
│
▼
orders-topic
│
┌───────────┴───────────┐
▼ ▼
inventory-subscription notification-subscription
│ │
▼ ▼
Inventory Service Notification Service
1. What Does "Publish an Event" Mean?
Suppose the Order Service creates an order.
After successful creation, it publishes:
OrderCreatedIntegrationEvent
The Order Service is the publisher:
Order Service
↓
OrderCreatedIntegrationEvent
↓
Message Broker
It does not need to directly call Inventory or Notification.
2. What Does "Consume an Event" Mean?
A consumer/subscriber listens for events and performs its own business logic.
For example:
OrderCreatedIntegrationEvent
│
├──> Inventory Service
│ ↓
│ Reserve stock
│
└──> Notification Service
↓
Send email
So:
Publish
↓
Send event to broker
Consume
↓
Receive event from broker and process it
3. Example Projects
Assume we have three ASP.NET Core applications:
MicroservicesDemo
│
├── OrderService
│
├── InventoryService
│
└── NotificationService
We'll use:
Azure Service Bus
Topic:
orders-topic
Subscriptions:
inventory-subscription
notification-subscription
4. Required NuGet Package
Install in the services that communicate with Azure Service Bus:
dotnet add package Azure.Messaging.ServiceBus
Namespace:
using Azure.Messaging.ServiceBus;
5. Create the Integration Event
Create:
public class OrderCreatedIntegrationEvent
{
public Guid EventId { get; set; }
public int OrderId { get; set; }
public int CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CreatedAt { get; set; }
}
Example message:
{
"eventId": "a76d4030-bdb0-43fb-a125-428715c2d22e",
"orderId": 1001,
"customerId": 501,
"totalAmount": 2500,
"createdAt": "2026-09-29T10:30:00Z"
}
6. Publisher — Order Service
Let's first build the publisher.
Create an interface:
public interface IEventPublisher
{
Task PublishAsync<T>(
T integrationEvent,
CancellationToken cancellationToken = default);
}
This prevents the controller from directly depending on Azure Service Bus.
Instead of:
Controller
↓
Azure Service Bus
we have:
Controller
↓
IEventPublisher
↓
AzureServiceBusEventPublisher
↓
Azure Service Bus
7. Implement the Publisher
Create:
using Azure.Messaging.ServiceBus;
using System.Text.Json;
public class AzureServiceBusEventPublisher
: IEventPublisher
{
private readonly ServiceBusClient _client;
private readonly IConfiguration _configuration;
public AzureServiceBusEventPublisher(
ServiceBusClient client,
IConfiguration configuration)
{
_client = client;
_configuration = configuration;
}
public async Task PublishAsync<T>(
T integrationEvent,
CancellationToken cancellationToken = default)
{
string topicName =
_configuration["AzureServiceBus:TopicName"]!;
ServiceBusSender sender =
_client.CreateSender(topicName);
string json =
JsonSerializer.Serialize(integrationEvent);
var message =
new ServiceBusMessage(json)
{
Subject = typeof(T).Name
};
await sender.SendMessageAsync(
message,
cancellationToken);
}
}
8. appsettings.json
In Order Service:
{
"AzureServiceBus": {
"ConnectionString": "<YOUR-AZURE-SERVICE-BUS-CONNECTION-STRING>",
"TopicName": "orders-topic"
}
}
For learning/local testing this is simple.
For production, avoid committing the real connection string to source control.
Prefer:
Managed Identity
+
Microsoft Entra ID
or secure configuration such as Azure Key Vault/environment configuration.
9. Register Service Bus in Program.cs
Order Service:
using Azure.Messaging.ServiceBus;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
string connectionString =
builder.Configuration[
"AzureServiceBus:ConnectionString"]!;
builder.Services.AddSingleton(
new ServiceBusClient(connectionString));
builder.Services.AddSingleton<
IEventPublisher,
AzureServiceBusEventPublisher>();
var app = builder.Build();
app.MapControllers();
app.Run();
10. Create Order API
Now create:
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IEventPublisher _eventPublisher;
public OrdersController(
IEventPublisher eventPublisher)
{
_eventPublisher = eventPublisher;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(
CancellationToken cancellationToken)
{
// Assume order was saved into database.
int orderId = 1001;
var orderCreatedEvent =
new OrderCreatedIntegrationEvent
{
EventId = Guid.NewGuid(),
OrderId = orderId,
CustomerId = 501,
TotalAmount = 2500,
CreatedAt = DateTime.UtcNow
};
await _eventPublisher.PublishAsync(
orderCreatedEvent,
cancellationToken);
return Ok(new
{
OrderId = orderId,
Message = "Order created successfully"
});
}
}
11. Publisher Flow
When the client calls:
POST /api/orders
the flow is:
Client
│
│ POST /api/orders
▼
OrdersController
│
▼
Create Order
│
▼
Save Database
│
▼
Create OrderCreatedIntegrationEvent
│
▼
IEventPublisher
│
▼
AzureServiceBusEventPublisher
│
▼
orders-topic
At this point, the event has been published.
12. Consumer — Inventory Service
Now let's create the consumer.
Inventory Service should listen to:
Topic:
orders-topic
Subscription:
inventory-subscription
Architecture:
orders-topic
↓
inventory-subscription
↓
Inventory Service
13. Consumer appsettings.json
Inventory Service:
{
"AzureServiceBus": {
"ConnectionString": "<YOUR-AZURE-SERVICE-BUS-CONNECTION-STRING>",
"TopicName": "orders-topic",
"SubscriptionName": "inventory-subscription"
}
}
14. Create BackgroundService Consumer
ASP.NET Core provides:
BackgroundService
which is convenient for continuously listening to broker messages.
Create:
using Azure.Messaging.ServiceBus;
using System.Text.Json;
public class OrderCreatedConsumer
: BackgroundService
{
private readonly ServiceBusClient _client;
private readonly IConfiguration _configuration;
private ServiceBusProcessor? _processor;
public OrderCreatedConsumer(
ServiceBusClient client,
IConfiguration configuration)
{
_client = client;
_configuration = configuration;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
string topicName =
_configuration["AzureServiceBus:TopicName"]!;
string subscriptionName =
_configuration[
"AzureServiceBus:SubscriptionName"]!;
_processor =
_client.CreateProcessor(
topicName,
subscriptionName);
_processor.ProcessMessageAsync +=
ProcessMessageAsync;
_processor.ProcessErrorAsync +=
ProcessErrorAsync;
await _processor.StartProcessingAsync(
stoppingToken);
}
private async Task ProcessMessageAsync(
ProcessMessageEventArgs args)
{
string json =
args.Message.Body.ToString();
var orderCreated =
JsonSerializer.Deserialize<
OrderCreatedIntegrationEvent>(json);
if (orderCreated is not null)
{
Console.WriteLine(
$"Order received: {orderCreated.OrderId}");
Console.WriteLine(
$"Amount: {orderCreated.TotalAmount}");
// Inventory business logic
// ReserveInventory(orderCreated.OrderId);
}
await args.CompleteMessageAsync(
args.Message);
}
private Task ProcessErrorAsync(
ProcessErrorEventArgs args)
{
Console.WriteLine(
$"Service Bus Error: {args.Exception.Message}");
return Task.CompletedTask;
}
public override async Task StopAsync(
CancellationToken cancellationToken)
{
if (_processor is not null)
{
await _processor.StopProcessingAsync(
cancellationToken);
await _processor.DisposeAsync();
}
await base.StopAsync(cancellationToken);
}
}
15. Register Consumer in Program.cs
Inventory Service:
using Azure.Messaging.ServiceBus;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
string connectionString =
builder.Configuration[
"AzureServiceBus:ConnectionString"]!;
builder.Services.AddSingleton(
new ServiceBusClient(connectionString));
builder.Services.AddHostedService<
OrderCreatedConsumer>();
var app = builder.Build();
app.MapControllers();
app.Run();
The important line is:
builder.Services.AddHostedService<
OrderCreatedConsumer>();
ASP.NET Core starts the background worker when the application starts.
16. Complete Consumer Flow
When Inventory Service starts:
ASP.NET Core Starts
↓
OrderCreatedConsumer starts
↓
Connect to Azure Service Bus
↓
Listen to orders-topic
↓
inventory-subscription
↓
Wait for messages
When an event arrives:
OrderCreatedIntegrationEvent
↓
inventory-subscription
↓
OrderCreatedConsumer
↓
Deserialize JSON
↓
Inventory business logic
↓
Complete / ACK message
17. Why CompleteMessageAsync?
Notice:
await args.CompleteMessageAsync(
args.Message);
This tells Azure Service Bus:
I successfully processed this message.
Conceptually:
Receive Message
↓
Process
↓
Successful?
┌──┴──┐
YES NO
│ │
▼ ▼
Complete Retry / redelivery
Don't acknowledge/complete a message before the important business processing has successfully finished.
18. Notification Service
Now suppose Notification Service also needs the same event.
Create another subscription:
notification-subscription
Then:
orders-topic
│
┌───────────┴───────────┐
▼ ▼
inventory-subscription notification-subscription
│ │
▼ ▼
Inventory Service Notification Service
The Notification consumer can use:
_client.CreateProcessor(
"orders-topic",
"notification-subscription");
Its business logic might be:
Console.WriteLine(
$"Sending confirmation for Order {orderCreated.OrderId}");
19. Why Separate Subscriptions?
Don't make Inventory and Notification compete on one subscription if both need the event.
Wrong:
orders-topic
↓
one-subscription
│
├──> Inventory
└──> Notification
They compete ❌
Correct:
orders-topic
│
├──> inventory-subscription
│ ↓
│ Inventory
│
└──> notification-subscription
↓
Notification
Now both independently receive OrderCreated.
20. Full End-to-End Flow
The complete architecture is:
Client
│
│ POST /api/orders
▼
┌───────────────┐
│ Order Service │
└───────┬───────┘
│
Save Order
│
▼
OrderCreatedIntegrationEvent
│
│ Publish
▼
┌──────────────────┐
│ orders-topic │
└────────┬─────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
inventory-subscription notification-subscription
│ │
▼ ▼
┌─────────────────┐ ┌────────────────────┐
│Inventory Service│ │Notification Service│
└────────┬────────┘ └─────────┬──────────┘
│ │
▼ ▼
Reserve Stock Send Email
│ │
▼ ▼
Complete Complete
This is asynchronous event-driven communication.
21. Very Important Production Problem
The previous example is excellent for understanding publishing and consuming, but this production code has a potential problem:
await dbContext.SaveChangesAsync();
await eventPublisher.PublishAsync(
orderCreatedEvent);
Suppose:
SaveChangesAsync
↓
SUCCESS
PublishAsync
↓
FAILED
Now:
Order Database
↓
Order exists
Azure Service Bus
↓
No event
Inventory and Notification never receive OrderCreated.
22. Industry Approach — Transactional Outbox
A common production solution is the Transactional Outbox Pattern.
Instead of immediately publishing:
Database
↓
Broker
save both:
Order
+
Integration Event
in the same database transaction.
Order Service
↓
┌─────────────────────────────┐
│ SQL Transaction │
│ │
│ INSERT Order │
│ │
│ INSERT OutboxMessage │
│ │
│ COMMIT │
└─────────────┬───────────────┘
│
▼
Outbox Worker
│
▼
Azure Service Bus
│
▼
orders-topic
If the database transaction succeeds:
Order ✓
Outbox Event ✓
If it fails:
Order ✗
Outbox Event ✗
This closes the dangerous gap between saving business data and recording the event that needs to be published.
23. Outbox Table
For example:
CREATE TABLE OutboxMessages
(
Id UNIQUEIDENTIFIER PRIMARY KEY,
EventType NVARCHAR(200) NOT NULL,
Payload NVARCHAR(MAX) NOT NULL,
CreatedAtUtc DATETIME2 NOT NULL,
ProcessedAtUtc DATETIME2 NULL
);
When creating an order:
BEGIN TRANSACTION
INSERT Orders...
INSERT OutboxMessages...
COMMIT
A background worker reads:
SELECT *
FROM OutboxMessages
WHERE ProcessedAtUtc IS NULL;
and publishes them.
24. Consumer Idempotency
Another production issue is duplicate delivery.
Imagine:
Consumer receives Event ABC123
↓
Updates Inventory
↓
Processing successful
↓
Network problem before Complete
↓
Broker redelivers ABC123
Without protection:
Inventory reduced once
↓
Inventory reduced again ❌
Therefore consumers should often be idempotent.
For example:
EventId = ABC123
↓
Already processed?
┌──┴───┐
YES NO
│ │
Ignore Process
You could maintain:
ProcessedMessages
EventId
------------------------------------
ABC123
DEF456
GHI789
25. Error Handling
Don't simply swallow consumer exceptions.
Conceptually:
Message
↓
Consumer
↓
Try Process
│
├── Success
│ ↓
│ Complete
│
└── Failure
↓
Retry/redelivery
↓
Still failing
↓
Dead-Letter Queue
Azure Service Bus supports dead-lettering for messages that cannot be successfully processed.
26. Event Contract
Avoid publishing your EF Core entity directly:
await publisher.PublishAsync(order); // Avoid
Instead:
var integrationEvent =
new OrderCreatedIntegrationEvent
{
EventId = Guid.NewGuid(),
OrderId = order.Id,
CustomerId = order.CustomerId,
TotalAmount = order.TotalAmount,
CreatedAt = DateTime.UtcNow
};
Remember:
EF Entity
≠
Integration Event
Your integration event is an external contract.
27. Event Metadata
A production event commonly includes metadata such as:
public abstract class IntegrationEvent
{
public Guid EventId { get; init; }
= Guid.NewGuid();
public DateTime OccurredAtUtc { get; init; }
= DateTime.UtcNow;
public string? CorrelationId { get; init; }
public int Version { get; init; } = 1;
}
Then:
public class OrderCreatedIntegrationEvent
: IntegrationEvent
{
public int OrderId { get; init; }
public int CustomerId { get; init; }
public decimal TotalAmount { get; init; }
}
Useful metadata includes:
EventId
EventType
OccurredAt
CorrelationId
Version
Source
28. Correlation ID
Suppose one order generates many operations:
CreateOrder
↓
OrderCreated
↓
InventoryReserved
↓
PaymentCompleted
↓
ShipmentCreated
Using the same:
CorrelationId = CORR-1001
allows logs and distributed traces to associate the operations:
CORR-1001
│
├── Order Service
├── Inventory Service
├── Payment Service
└── Shipping Service
This becomes very important when troubleshooting distributed systems.
29. Event Versioning
Suppose the first event is:
{
"orderId": 1001,
"amount": 2500
}
Later you add:
{
"orderId": 1001,
"amount": 2500,
"currency": "INR"
}
Existing consumers may still expect the old contract.
Therefore integration events require a schema-evolution strategy.
Prefer backward-compatible changes where possible.
For larger breaking changes, you might explicitly version the contract:
OrderCreatedV1
OrderCreatedV2
30. Azure Service Bus vs RabbitMQ vs Kafka
The ASP.NET Core business architecture remains conceptually similar:
ASP.NET Core
↓
Event Publisher
↓
Messaging Infrastructure
↓
Event Consumer
But implementation differs.
Azure Service Bus
Publisher
↓
Topic
↓
Subscriptions
↓
Consumers
RabbitMQ
Publisher
↓
Exchange
↓
Bindings
↓
Queues
↓
Consumers
Kafka
Producer
↓
Topic
↓
Partitions
↓
Consumer Groups
31. Recommended Layering
For a Clean Architecture microservice, a useful structure is:
OrderService
│
├── Domain
│
│ └── Events
│
├── Application
│
│ ├── IntegrationEvents
│
│ └── Abstractions
│ └── IEventPublisher.cs
│
├── Infrastructure
│
│ └── Messaging
│ └── AzureServiceBusEventPublisher.cs
│
└── API
├── Controllers
└── Program.cs
Dependency concept:
API
↓
Application
↓
Domain
Infrastructure
↓
Application abstractions
Azure.Messaging.ServiceBus belongs naturally in the Infrastructure implementation rather than your Domain layer.
32. Advantages
Publishing/consuming events provides:
- Loose coupling
- Asynchronous processing
- Independent microservices
- Independent scaling
- Fault isolation
- Traffic buffering
- Multiple subscribers
- Event-Driven Architecture
- Easier extension with new consumers
For example:
OrderCreated
├──> Inventory
├──> Notification
└──> Analytics
Later:
OrderCreated
├──> Inventory
├──> Notification
├──> Analytics
└──> Loyalty ← New consumer
The publisher may remain unchanged.
33. Disadvantages
It also introduces:
- Message broker infrastructure
- Eventual consistency
- Duplicate processing
- Retry complexity
- Dead-letter handling
- Event ordering concerns
- Schema evolution
- Distributed tracing complexity
- Harder debugging
- Operational monitoring requirements
So event-driven communication is powerful, but it isn't automatically simpler than HTTP.
34. Key Points
For interview preparation, remember:
- A publisher produces an event.
- A consumer/subscriber processes an event.
- ASP.NET Core does not itself provide a distributed message broker.
- Common brokers/platforms are Azure Service Bus, RabbitMQ, and Kafka.
- Azure Service Bus Pub/Sub uses Topics + Subscriptions.
Azure.Messaging.ServiceBusis the .NET client package used in this example.ServiceBusSenderpublishes messages.ServiceBusProcessorcan receive/process messages.- ASP.NET Core
BackgroundServiceis convenient for long-running consumers. - Each independent service should generally have its own subscription when all need the event.
- Complete/ACK a message after successful processing.
- Consumers should account for duplicate delivery through idempotency.
- Failed messages require retry/dead-letter handling.
- Don't publish EF/domain entities directly as integration contracts.
- Use dedicated Integration Event contracts.
- Use Correlation IDs for tracing.
- Plan for event schema/version evolution.
- For reliable database + event publication, the Transactional Outbox Pattern is a common production design.
35. Interview Questions and Answers
Q1. How do you publish an event in ASP.NET Core?
Answer:
Create an Integration Event object, serialize it, and use a messaging client such as ServiceBusSender to publish it to an Azure Service Bus Topic.
Order Service
↓
OrderCreatedIntegrationEvent
↓
ServiceBusSender
↓
orders-topic
Q2. How do you consume an event?
Answer:
Create a consumer that listens to a queue or topic subscription. With Azure Service Bus, ServiceBusProcessor can receive messages, and an ASP.NET Core BackgroundService can host the consumer continuously.
Q3. Why use BackgroundService?
Answer:
A consumer needs to run continuously in the background and listen for incoming messages.
BackgroundService integrates that long-running worker with ASP.NET Core's host lifecycle.
Q4. What happens after successful processing?
Answer:
The consumer completes/acknowledges the message.
For Azure Service Bus:
await args.CompleteMessageAsync(
args.Message);
Q5. What happens if processing fails?
Answer:
The message should not be treated as successfully completed. Depending on broker configuration and application logic, it can be retried/redelivered and eventually dead-lettered if it repeatedly fails.
Q6. Why do consumers need idempotency?
Answer:
Because distributed messaging can result in duplicate delivery or reprocessing.
An idempotent consumer ensures processing the same event multiple times doesn't incorrectly repeat the business effect.
Q7. What is the Transactional Outbox Pattern?
Answer:
It stores the business data change and the event to be published in the same database transaction.
A separate background worker later publishes the outbox event to the broker.
Database Transaction
│
├── Order
└── Outbox Event
↓
Commit
↓
Outbox Worker
↓
Message Broker
This prevents the common inconsistency where the database succeeds but publishing fails.
Q8. Should we publish EF Core entities?
Answer:
Generally, no.
Use dedicated Integration Event contracts so consumers are not coupled to your internal database/domain model.
Q9. Can multiple services consume the same event?
Answer:
Yes.
With Azure Service Bus, give independent consumers their own subscriptions:
orders-topic
├──> inventory-subscription
├──> notification-subscription
└──> analytics-subscription
All can independently receive OrderCreated.
Interview-Friendly Final Answer
In ASP.NET Core, events are typically published and consumed using a message broker such as Azure Service Bus, RabbitMQ, or Kafka. For Azure Service Bus, the publisher creates an Integration Event, serializes it, and publishes it to a Topic using
ServiceBusSender. Each interested microservice has its own Subscription and can run aBackgroundServiceusingServiceBusProcessorto continuously receive and process messages. After successful processing, the consumer completes the message. In production, the design should also handle retries, dead-letter messages, idempotent consumers, correlation IDs, event versioning, and reliable publication through patterns such as Transactional Outbox.