← Back to Articles         
Publishing & Consuming Events

Publishing & Consuming Events

Published on 29 Sep 2026     13 min read Microservices
Messaging & Event-Driven

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.ServiceBus is the .NET client package used in this example.
  • ServiceBusSender publishes messages.
  • ServiceBusProcessor can receive/process messages.
  • ASP.NET Core BackgroundService is 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 a BackgroundService using ServiceBusProcessor to 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.

← Back to Articles