ARTICLE

Implementing an Event-Based Collaboration using HTTP

From Microservices in .NET, 2nd Edition by Christian Horsdal Gammelgaard

Figure 1. The event-based collaboration in the Loyalty Program microservice is the subscription to the event feed in the Special Offers microservice.

Implementing an event feed

The Special Offers microservice implements its event feed by exposing an endpoint — /events—that returns a list of sequentially numbered events. The endpoint can take two query parameters—start and end—that specify a range of events. For example, a request to the event feed can look like this:

GET /events?start=10&end=110 HTTP/1.1

Host: localhost:5002
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

[
{
"sequenceNumber": 1,
"occuredAt": "2020-06-16T20:13:53.6678934+00:00",
"name": "SpecialOfferCreated",
"content": {
"description": "Best deal ever!!!",
"id": 0
}
},
{
"sequenceNumber": 2,
"occuredAt": "2020-06-16T20:14:22.6229836+00:00",
"name": "SpecialOfferCreated",
"content": {
"description": "Special offer - just for you",
"id": 1
}
},
{
"sequenceNumber": 3,
"occuredAt": "2020-06-16T20:14:39.841415+00:00",
"name": "SpecialOfferCreated",
"content": {
"description": "Nice deal",
"id": 2
}
},
{
"sequenceNumber": 4,
"occuredAt": "2020-06-16T20:14:47.3420926+00:00",
"name": "SpecialOfferUpdated",
"content": {
"oldOffer": {
"description": "Nice deal",
"id": 2
},
"newOffer": {
"description": "Best deal ever - JUST GOT BETTER",
"id": 0
}
}
},
{
"sequenceNumber": 5,
"occuredAt": "2020-06-16T20:14:51.8986625+00:00",
"name": "SpecialOfferRemoved",
"content": {
"offer": {
"description": "Special offer - just for you",
"id": 1
}
}
}
]
namespace SpecialOffers.Events
{
using System.Linq;
using Microsoft.AspNetCore.Mvc;

[Route(("/events"))]
public class EventFeedController : Controller
{
private readonly IEventStore eventStore;

public EventFeedController(IEventStore eventStore)
{
this.eventStore = eventStore;
}

[HttpGet("")]
public ActionResult<EventFeedEvent[]> GetEvents([FromQuery] int start, [FromQuery] int end)
{
if (start < 0 || end < start)
return BadRequest();

return this.eventStore.GetEvents(start, end).ToArray();
}
}
}
public class EventFeedEvent
{
public long SequenceNumber { get; }
public DateTimeOffset OccuredAt { get; }
public string Name { get; }
public object Content { get; }

public EventFeedEvent(
long sequenceNumber,
DateTimeOffset occuredAt,
string name,
object content)
{
this.SequenceNumber = sequenceNumber;
this.OccuredAt = occuredAt;
this.Name = name;
this.Content = content;
}
}

Creating an event-subscriber process

Subscribing to an event feed essentially means you’ll poll the events endpoint of the microservice you subscribe to. At intervals, you’ll send an HTTP GET request to the /events endpoint to check whether there are any events you haven’t processed yet.

  • A simple console application that reads one batch of events
  • We will use a Kubernetes cron job to run the console application at intervals
PS> dotnet new console -n EventConsumer
PS> dotnet run

Subscribing to an event feed

You now have a EventConsumer console application. All it has to do is read one batch of events and track where the starting point of the next batch of events is. This is done as follows:

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;

var start = await GetStartIdFromDatastore(); ❶
var end = 100;
var client = new HttpClient();
client.DefaultRequestHeaders
.Accept
.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var resp = await client.GetAsync( ❷
new Uri($"http://special-offers:5002/events?start={start}&end={end}"));
await ProcessEvents(await resp.Content.ReadAsStreamAsync()); ❸
await SaveStartIdToDataStore(start); ❹

Task<long> GetStartIdFromDatastore(){...}
async Task ProcessEvents(Stream content){...}
Task SaveStartIdToDataStore(long startId){...}
async Task ProcessEvents(Stream content)
{
var events =
await JsonSerializer.DeserializeAsync<SpecialOfferEvent[]>(content)
?? new SpecialOfferEvent[0];
foreach (var @event in events)
{
Console.WriteLine(@event); ❶
start = Math.Max(start, @event.SequenceNumber + 1); ❷
}
}
  • This method keeps track of which events have been handled #2. This makes sure you don’t request events from the feed that you’ve already processed.
  • We treat the Content property on the events as dynamic #1. As you saw earlier, not all events carry the same data in the Content property, so treating it as dynamic allows you to access the properties you need on .Content and not care about the rest. This is a sound approach because you want to be liberal in accepting incoming data—it shouldn’t cause problems if the Special Offers microservice decides to add an extra field to the event JSON. As long as the data you need is there, the rest can be ignored.
  • The events are deserialized into the type SpecialOfferEvent. This is a different type than the EventFeedEvent type used to serialize the events in Special Offers. This is intentional and is done because the two microservices don’t need to have the exact same view of the events. As long as Loyalty Program doesn’t depend on data that isn’t there, all is well.
public record SpecialOfferEvent(
long SequenceNumber,
DateTimeOffset OccuredAt,
string Name,
object Content);

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Manning Publications

Follow Manning Publications on Medium for free content and exclusive discounts.