Generic client for sending events to Event Grid

To support the use of Event Grid in our application architecture we decided to build a generic Event Grid client to send our events. This client enforces some practices like the use of base events and error logging.

Event Grid event schema

Event Grid events have a set schema which needs to be send and which Event Grid will again route to subscribers. The json looks like this (this is an event defined by Azure Storage):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[
{
"topic": "/subscriptions/{subscription-id}/resourceGroups/Storage/providers/Microsoft.Storage/storageAccounts/xstoretestaccount",
"subject": "/blobServices/default/containers/oc2d2817345i200097container/blobs/oc2d2817345i20002296blob",
"eventType": "Microsoft.Storage.BlobCreated",
"eventTime": "2017-06-26T18:41:00.9584103Z",
"id": "831e1650-001e-001b-66ab-eeb76e069631",
"data": {
"api": "PutBlockList",
"clientRequestId": "6d79dbfb-0e37-4fc4-981f-442c9ca65760",
"requestId": "831e1650-001e-001b-66ab-eeb76e000000",
"eTag": "0x8D4BCC2E4835CD0",
"contentType": "application/octet-stream",
"contentLength": 524288,
"blobType": "BlockBlob",
"url": "https://oc2d2817345i60006.blob.core.windows.net/oc2d2817345i200097container/oc2d2817345i20002296blob",
"sequencer": "00000000000004420000000000028963",
"storageDiagnostics": {
"batchId": "b68529f3-68cd-4744-baa4-3c0498ec19f0"
}
},
"dataVersion": "",
"metadataVersion": "1"
}
]

Most fields are probably pretty logical, but I want to call out the data field, because this is where you can put your own “Event” data. To make it easy to work with I made a POCO class to represent the event schema. This way you can serialize back and forth easily between json and a class. The Data field is represented as a generic T, because this could be of any type. You could add a generic class or a JObject if you need a generic representation of the event (if you want to receive multiple different event types for example.

1
2
3
4
5
6
7
8
9
10
11
public class GridEvent<T> where T : class
{
public string Id { get; set; }
public string Subject { get; set; }
public string EventType { get; set; }
public T Data { get; set; }
public DateTime EventTime { get; set; }
public string Topic { get; set; }
public string DataVersion { get; set; }
public string MetadataVersion { get; set; }
}
I made some base classes for specific events which would be in the Data block. For example every event in our organization contains a SourceApplicationName and we have the validationcode added in this event to support easier handling of returning the validationcode when a Event Grid event subscription is added. We have another base event that inherits from that one for user specific events with a UserId. All events need to inherit from these base events, something the generic client enforces.
1
2
3
4
5
6
7
8
9
10
11
public class AppGridEvent
{
public string SourceApplicationName { get; set; }
public string ValidationCode { get; set; }
...
}
public class AppGridUserEvent : AppGridEvent
{
public Guid UserId { get; set; }
...
}

Generic client

Sending events is easy. It is just a HTTP Post, but still there is value in having a generic client to do this. It can handle a couple of things for you that make life easier.

Configuration
Our clients constructor takes a couple of configuration values:
TopicEndpointUrl: The url where events will be send
SasToken: The token used to authenticate with EventGrid (so you are allowed to send the event)
ApplicationName: Name of the calling application so we always know the source (this is automatically set by the client)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class EventClient : IEventClient
{
private readonly EventClientConfig _config;
private readonly TelemetryClient _telemetryClient;
private readonly HttpClient _client;

public EventClient(EventClientConfig config, TelemetryClient telemetryClient)
{
if (config == null)
throw new ArgumentNullException("config");

if (config.TopicEndpointUrl == null)
throw new ArgumentNullException("TopicEndpointUrl cannot be null");

if (config.SasToken == null)
throw new ArgumentNullException("SasToken cannot be null");

if (config.ApplicationName == null)
throw new ArgumentNullException("ApplicationName cannot be null");

_config = config;
_telemetryClient = telemetryClient;

_client = new HttpClient();
client.DefaultRequestHeaders.Add("aeg-sas-key", _config.SasToken);
client.DefaultRequestHeaders.UserAgent.ParseAdd(_config.ApplicationName);
}

public async Task<bool> SendEventAsync<T>(T eventData) where T : AppGridEvent
{
...
}
}
These settings come from an object out of our appsettings.json, but obviously could come from where ever you like.

In the constructor we also initialize the HttpClient. Note that every HttpClient constructor call registers a new port on your machine, so our EventClient needs to be used as a singleton or static in your application.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if (eventData == null)
throw new ArgumentNullException("eventData");

var gridEvent = new GridEvent<T>()
{
Id = Guid.NewGuid().ToString(),
EventType = typeof(T).Name,
EventTime = DateTime.UtcNow,
Subject = eventData.Subject,
Data = eventData,
DataVersion = ""
};

gridEvent.Data.SourceApplicationName = _config.ApplicationName;

string json = JsonConvert.SerializeObject(new List<GridEvent<T>>() { gridEvent });
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, _config.TopicEndpointUrl)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};

var result = await _client.SendAsync(request);

The client receives a generic object of base type AppGridEvent which contains the base fields like ApplicationName. This base type is serialized in json and a HttpRequestMessage is created. This message is send to EventGrid and we record if a successful message is return.

One open point right now is versioning. When the time comes when we need it, I plan to get the version from an attribute on the POCO class.

Catching exceptions

In our application events that we throw don’t need to be transaction. We want as many as possible, but if we sometimes miss some, that is fine. So we catch any errors that occur on sending of the event, because we don’t want this to bubble up to the user. This works very well for us, because we rather miss an event than the user being impacted by one of these errors. But keep in mind, this might not work for everyone/all type of events.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try
{
...
if (!result.IsSuccessStatusCode)
{
_telemetryClient.TrackException(new Exception($"SendEventAsync resulted in invalid statuscode {result.StatusCode.ToString()}"));
}
return result.IsSuccessStatusCode;
}
catch (Exception exc)
{
_telemetryClient.TrackException(exc);
return false;
}
In case of an incorrect result status code we log this code, but you could also throw an exception here. Keep in mind that you will need to check this, just catching exceptions is not enough to see all failed requests.

Sharing of events (event types)

At the moment we use a nuget package to share events definitions (in the form of the POCO classes) between the different applications that use this event. This package is stored in a private Visual Studio online package repositry. This works very well and enforces that when the event is changed (and the nuget package is updated) the applications break. But it does somewhat break the principle that the source application is the owner of the schema.

Until now this approach has been working well for us. But I am always open for suggestions, so if you see possible improvements, please let me know!

Share