Sometimes, it makes sense to create a simple wrapper that smooths out the interface of client components that facilitate integration with external systems. In this case, Event Grid and the use case of publishing a single event. Sure, there are edge cases where I might need to dive into the details of the client’s specific nature to optimize for performance — such as when doing bulk publishing — but most of the time, when building microservices, I simply need to publish an object payload to a specific event type. That means I really only need to pass in two things: the event type (a magic string)and the payload (an object).
I often need to dependency inject this publisher component, and I probably won’t be using a different Event Grid Topic endpoint — at least within the same microservice — unless I’m implementing dual-channel Event Grid (which I might write more about later) to maintain clear isolation between public events and internal chatter. Either way, both approaches benefit from a simple abstraction of EventGridPublisherClient.
Designing a Simple Abstraction
Rather than interacting with EventGridPublisherClient directly, we define a clean, minimal interface:
public interface IEventPublisher
{
Task PublishAsync<T>(string eventType, T payload);
}
Keep it simple. You see what you get. Pass in an event type and an object, I will publish it says this little interface. This keeps things straightforward. The caller simply provides an event type (a string) and a payload (an object), and our wrapper handles the rest.
Implementing the Event Publisher
The implementation starts with dependency injection, ensuring our wrapper can leverage logging, telemetry, and the Event Grid client itself:
public class EventPublisher : IEventPublisher
{
private readonly ILogger<EventPublisher> _logger;
private readonly TelemetryClient _telemetryClient;
private readonly EventGridPublisherClient _eventGridPublisherClient;
public EventPublisher(
ILogger<EventPublisher> logger,
TelemetryClient telemetryClient,
EventGridPublisherClient eventGridPublisherClient
)
{
_logger = logger;
_telemetryClient = telemetryClient;
_eventGridPublisherClient = eventGridPublisherClient;
}
// TODO: moar!!!
}
Here I am simply allowing this component to take advantage of the dependency injection mechanism by injecting an ILogger, TelemetryClient and EventGridPublisherClient in through the constructor. The setup for the concrete instances of these classes need to be taken care of in the HostBuilder setup.
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices((context, services) => {
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
var configuration = context.Configuration;
// Get EventGrid Topic
var managedIdentityClientId = configuration["FUNCTION_MANAGED_IDENTITY"];
var topicEndpoint = configuration["EVENTGRID_INTERNAL_ENDPOINT"];
var managedIdentityCredential = new ManagedIdentityCredential(managedIdentityClientId);
services.AddSingleton(x => new EventGridPublisherClient(new Uri(topicEndpoint), managedIdentityCredential));
// PubSub
services.AddTransient<IEventPublisher, EventPublisher>();
})
.ConfigureLogging(logging =>
{
logging.Services.Configure<LoggerFilterOptions>(options =>
{
LoggerFilterRule defaultRule = options.Rules.FirstOrDefault(rule => rule.ProviderName
== "Microsoft.Extensions.Logging.ApplicationInsights.ApplicationInsightsLoggerProvider");
if (defaultRule is not null)
{
options.Rules.Remove(defaultRule);
}
});
})
.Build();
host.Run();
This is the basic setup that will get you off the ground with your ILogger and Application Insights all setup as well as the EventGridPublisherClient fully initialized and configured with the correct endpoint and the User Assigned Managed Identity — both pulled from environment variables.
Publishing an Event
Now, let’s implement the PublishAsync method that actually sends an event:
public async Task PublishAsync<T>(string eventType, T payload)
{
var eventSource = "/cloudevents/foo/source";
var event1 = new CloudEvent(eventSource, eventType, payload);
List<CloudEvent> eventsList = new List<CloudEvent>
{
event1
};
var eventGridResponse = await _eventGridPublisherClient.SendEventsAsync(eventsList);
if (eventGridResponse.IsError)
{
_logger.LogError("Unable to publish eventgrid event");
}
else
{
var payloadData = JsonSerializer.Serialize(payload);
var stateFileProps = new Dictionary<string, string>()
{
{ "EventType", eventType },
{ "Payload", payloadData }
};
_telemetryClient.TrackEvent("EventPublisher.PublishEvent");
}
}
This method:
- Creates a CloudEvent with a standard schema.
- Publishes the event to Event Grid.
- Logs errors if publishing fails.
- Tracks telemetry for observability — might want to at least remove the payload after you’ve done your initial testing.
We should of course be using the CloudEvents schema so make sure when you provision your EventGrid Topic you do so like this:
resource "azurerm_eventgrid_topic" "main" {
name = "evgt-${var.name}-${var.location}"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
input_schema = "CloudEventSchemaV1_0"
tags = var.tags
}
By default, if the input_schema attribute is left blank, the azurerm Terraform provider will use the old “EventGrid Schema” instead of the new hotness that is the “Cloud Events Schema”.
Testing with the Event Publisher
Now that we have a clean abstraction, testing becomes much easier. By targeting the IEventPublisher interface, we can eliminate actual Event Grid publishing in our unit tests and mock the behavior instead.
Using Moq, we create a mock instance of IEventPublisher:
protected readonly Mock<IEventPublisher> _mockEventPublisher = new Mock<IEventPublisher>();
Then, we stub the PublishAsync method to do nothing:
_mockEventPublisher
.Setup(m => m.PublishAsync(It.IsAny<string>(), It.IsAny<object>()))
.Returns(Task.CompletedTask);
With this setup, our tests can run without actually sending events, making them faster and more predictable — it’s like we’re swinging at thin air!
Conclusion
By wrapping EventGridPublisherClient in a simple abstraction, we make publishing events easier, more testable, and more maintainable. This approach works seamlessly for microservices that only need to send individual eventswithout worrying about bulk operations.
For teams that need stricter event separation, the wrapper can be extended to support dual-channel Event Grid, isolating public events from internal events — something I may explore in a future article.
Top comments (0)