EventBridge from .NET
EventBridge is AWS's serverless event bus. You publish structured events, define rules that match patterns in those events, and route them to targets (Lambda, SQS, Step Functions, other accounts). It's the backbone of decoupled architectures on AWS.
From .NET, the main challenges are:
- Defining a consistent event schema across services
- Serializing/deserializing events without reflection (for AoT)
- Consuming events in Lambda handlers with proper typing
Publishing Events
Basic event publishing
using Amazon.EventBridge;
using Amazon.EventBridge.Model;
using System.Text.Json;
var client = new AmazonEventBridgeClient();
var orderEvent = new OrderPlacedEvent
{
OrderId = "ORD-12345",
CustomerId = "CUST-789",
Amount = 249.99m,
Items = new[] { "PROD-001", "PROD-042" },
};
await client.PutEventsAsync(new PutEventsRequest
{
Entries = new List<PutEventsRequestEntry>
{
new()
{
EventBusName = "my-app-events",
Source = "com.myapp.orders",
DetailType = "OrderPlaced",
Detail = JsonSerializer.Serialize(orderEvent, AppJsonContext.Default.OrderPlacedEvent),
}
}
});
AoT-compatible serialization
[JsonSerializable(typeof(OrderPlacedEvent))]
[JsonSerializable(typeof(OrderCancelledEvent))]
[JsonSerializable(typeof(PaymentProcessedEvent))]
internal partial class AppJsonContext : JsonSerializerContext { }
public record OrderPlacedEvent
{
public required string OrderId { get; init; }
public required string CustomerId { get; init; }
public required decimal Amount { get; init; }
public required string[] Items { get; init; }
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
}
Batch publishing
// PutEvents supports up to 10 entries per call
var entries = events.Select(e => new PutEventsRequestEntry
{
EventBusName = "my-app-events",
Source = "com.myapp.orders",
DetailType = e.GetType().Name,
Detail = JsonSerializer.Serialize(e, AppJsonContext.Default.GetTypeInfo(e.GetType())),
}).ToList();
// Chunk into batches of 10
foreach (var batch in entries.Chunk(10))
{
var response = await client.PutEventsAsync(new PutEventsRequest
{
Entries = batch.ToList(),
});
if (response.FailedEntryCount > 0)
{
// Handle failures β retry failed entries
foreach (var entry in response.Entries.Where(e => !string.IsNullOrEmpty(e.ErrorCode)))
{
logger.LogError("Event publish failed: {Code} {Message}",
entry.ErrorCode, entry.ErrorMessage);
}
}
}
Consuming Events in Lambda
EventBridge β Lambda handler
EventBridge wraps your event detail in a standard envelope. Your Lambda receives the full envelope:
using Amazon.Lambda.CloudWatchEvents;
public class OrderEventHandler
{
public async Task Handler(CloudWatchEvent<OrderPlacedEvent> eventBridgeEvent, ILambdaContext context)
{
var order = eventBridgeEvent.Detail;
context.Logger.LogInformation(
"Processing order {OrderId} for customer {CustomerId}, amount: {Amount}",
order.OrderId, order.CustomerId, order.Amount);
await ProcessOrderAsync(order);
}
}
Typed event handling with multiple event types
If one Lambda handles multiple event types from the same bus:
public async Task Handler(CloudWatchEvent<JsonElement> eventBridgeEvent, ILambdaContext context)
{
switch (eventBridgeEvent.DetailType)
{
case "OrderPlaced":
var placed = eventBridgeEvent.Detail.Deserialize(AppJsonContext.Default.OrderPlacedEvent)!;
await HandleOrderPlaced(placed);
break;
case "OrderCancelled":
var cancelled = eventBridgeEvent.Detail.Deserialize(AppJsonContext.Default.OrderCancelledEvent)!;
await HandleOrderCancelled(cancelled);
break;
default:
context.Logger.LogWarning("Unknown event type: {Type}", eventBridgeEvent.DetailType);
break;
}
}
Event Schema Design
A consistent schema makes life easier across services:
// Base event interface
public interface IDomainEvent
{
string EventId { get; }
DateTime Timestamp { get; }
string CorrelationId { get; }
}
public record OrderPlacedEvent : IDomainEvent
{
public string EventId { get; init; } = Ulid.NewUlid().ToString();
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
public required string CorrelationId { get; init; }
// Domain-specific fields
public required string OrderId { get; init; }
public required string CustomerId { get; init; }
public required decimal Amount { get; init; }
}
Event naming conventions
- Source: reverse domain notation:
com.myapp.orders,com.myapp.payments - DetailType: PascalCase event name:
OrderPlaced,PaymentFailed - Detail: the actual event payload
This maps naturally to the EventBridge rule pattern matching.
A Lightweight Event Publisher Abstraction
public interface IEventPublisher
{
Task PublishAsync<T>(string source, T domainEvent, CancellationToken ct = default)
where T : IDomainEvent;
}
public class EventBridgePublisher : IEventPublisher
{
private readonly IAmazonEventBridge _client;
private readonly string _busName;
public EventBridgePublisher(IAmazonEventBridge client, string busName)
{
_client = client;
_busName = busName;
}
public async Task PublishAsync<T>(string source, T domainEvent, CancellationToken ct = default)
where T : IDomainEvent
{
var response = await _client.PutEventsAsync(new PutEventsRequest
{
Entries = new List<PutEventsRequestEntry>
{
new()
{
EventBusName = _busName,
Source = source,
DetailType = typeof(T).Name,
Detail = JsonSerializer.Serialize(domainEvent,
AppJsonContext.Default.GetTypeInfo(typeof(T))!),
}
}
}, ct);
if (response.FailedEntryCount > 0)
{
throw new EventPublishException(
$"Failed to publish {typeof(T).Name}: {response.Entries[0].ErrorMessage}");
}
}
}
CDK Setup (C#)
using Amazon.CDK;
using Amazon.CDK.AWS.Events;
using Amazon.CDK.AWS.Events.Targets;
// Custom event bus
var bus = new EventBus(this, "AppEventBus", new EventBusProps
{
EventBusName = "my-app-events",
});
// Rule: route order events to processing Lambda
var orderRule = new Rule(this, "OrderPlacedRule", new RuleProps
{
EventBus = bus,
EventPattern = new EventPattern
{
Source = new[] { "com.myapp.orders" },
DetailType = new[] { "OrderPlaced" },
},
Targets = new[] { new LambdaFunction(orderProcessorFn) },
});
// Rule: send all events to audit queue with DLQ
var auditRule = new Rule(this, "AuditRule", new RuleProps
{
EventBus = bus,
EventPattern = new EventPattern
{
Source = new[] { "com.myapp.orders", "com.myapp.payments" },
},
Targets = new IRuleTarget[]
{
new SqsQueue(auditQueue, new SqsQueueProps
{
DeadLetterQueue = auditDlq,
}),
},
});
// Grant publisher
bus.GrantPutEventsTo(publisherFunction);
Testing Event-Driven Flows
Testing EventBridge flows locally is hard because the routing happens in the cloud. Strategies:
- Unit test the handler directly. Construct a
CloudWatchEvent<T>and call the handler - Integration test with a real bus. Publish an event, verify the target receives it (slow, but validates rules)
- Use EventBridge Archive and Replay. Replay production events through new handlers to validate changes
// Unit test example
[Fact]
public async Task Handler_ProcessesOrderPlacedEvent()
{
var handler = new OrderEventHandler(mockOrderService.Object);
var event = new CloudWatchEvent<OrderPlacedEvent>
{
Source = "com.myapp.orders",
DetailType = "OrderPlaced",
Detail = new OrderPlacedEvent
{
OrderId = "ORD-123",
CustomerId = "CUST-456",
Amount = 100m,
Items = new[] { "PROD-001" },
CorrelationId = "corr-789",
},
};
await handler.Handler(event, TestLambdaContext.Create());
mockOrderService.Verify(s => s.ProcessAsync(It.Is<OrderPlacedEvent>(
e => e.OrderId == "ORD-123")), Times.Once);
}
Tips
- Don't put large payloads in events. EventBridge has a 256KB limit per event. If you need to send large data, put it in S3 and include the S3 key in the event.
- Use the
CorrelationIdpattern. Pass a correlation ID through events so you can trace a business transaction across multiple services in CloudWatch Logs. - Dead-letter queues on every rule target. If a target fails, the event is lost without a DLQ. Always configure one.
- Prefer specific rules over catch-all handlers. Let EventBridge do the filtering rather than writing switch statements in your Lambda.
Further Reading
- AWS SDK for .NET: EventBridge
- EventBridge Developer Guide
- EventBridge event patterns
- EventBridge pricing
- AWS EventBridge overview: routing rules, schema discovery, and when to use EventBridge vs SNS/SQS
Related Blog Posts
Looking for hands-on help? View my .NET on AWS services β