DEV Community

Mark Tinderholt
Mark Tinderholt

Posted on

Simplifying Event Grid Publishing: A Lightweight Wrapper for Cleaner Code and Easier Testing

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);
}
Enter fullscreen mode Exit fullscreen mode

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!!!

}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

Then, we stub the PublishAsync method to do nothing:

_mockEventPublisher
  .Setup(m => m.PublishAsync(It.IsAny<string>(), It.IsAny<object>()))
  .Returns(Task.CompletedTask);
Enter fullscreen mode Exit fullscreen mode

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)