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 | [ |
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
11public 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; }
}1
2
3
4
5
6
7
8
9
10
11public 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
33public 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
{
...
}
}
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 | if (eventData == null) |
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
14try
{
...
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;
}
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!