ARTICLE

Implementing an Event-Based Collaboration using HTTP

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

Let’s talk about event-based collaboration. This is an important aspect of how microservices interact with each other.

Take 40% off Microservices in .NET, 2nd Edition by entering horsdal3 into the discount code ox at checkout at manning.com.

Imagine that we have a Loyalty Program microservice.

Figure 1 shows the collaborations that the Loyalty Program microservice is involved in. The Loyalty Program subscribes to events from Special Offers, and it uses the events to decide when to notify registered users about new special offers.

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

We’ll first look at how Special Offers exposes its events in a HTTP based feed. Then, we’ll return to Loyalty Program and add a second process to that service, which will be responsible for subscribing to events and handling events.

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

The response to this request might be the following, except that I’ve cut off the response after five events:

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
}
}
}
]

Notice that the events have different names (SpecialOfferCreated, SpecialOfferUpdated, and SpecialOfferRemoved) and the different types of events don’t have the same data fields. This is normal: different events carry different information. It’s also something you need to be aware of when you implement the subscriber in the Loyalty Program microservice. You can’t expect all events to have the exact same shape.

The implementation of the /events endpoint in the Special Offers microservice is a simple ASP.NET Core MVC controller.

Listing 1. Endpoint that reads and returns events

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();
}
}
}

You may notice, that GetEvents returns the result of eventStore.GetEvents. ASP.NET Core serializes it as an array. The EventFeedEvent is a class that carries a little metadata and a Content field that’s meant to hold the event data.

Listing 2. Event class that represents events

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;
}
}

The Content property is used for event-specific data and is where the difference between a SpecialOfferCreated event, a SpecialOfferUpdated and a SpecialOfferREmoved event appears. Each has its own type of object in Content.

This is all it takes to expose an event feed. This simplicity is the great advantage of using an HTTP-based event feed to publish events. Event-based collaboration can be implemented over a queue system, but that introduces another complex piece of technology that you have to learn to use and administer in production. That complexity is warranted in some situations, but certainly not always.

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.

We will implement this periodic polling as two main parts:

  • A simple console application that reads one batch of events
  • We will use a Kubernetes cron job to run the console application at intervals

Putting these two together they implement the event subscription: The cron job makes sure the console application runs at an interval and each time the console application runs it sends the HTTP GET request to check whether there are any events to process.

The first step in implementing an event-subscriber process is to create a console application with the following dotnet command:

PS> dotnet new console -n EventConsumer

and run it with dotnet too:

PS> dotnet run

The application is empty, so nothing interesting happens yet, but in the next section we will make it read events.

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:

Listing 3. Read a batch of events from an event feed

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){...}

Read the starting point of this batch from a database.

Send GET request to the event feed.

Read the starting point of this batch from a database.

Call method to process the events in this batch. ProcessEvents also updates the start variable.

With the code above the EventConsumer can read a batch of events, and every time it is called it reads the next batch of events. The remaining part is to process the events:

Listing 4. Deserializing and then handling events

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 is where the event would be processed.

Keeps track of the highest event number handled.

There are a few things to notice here:

  • 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.

The SpecialOfferEvent type used here is simple and contains only the fields used in Loyalty Program:

public record SpecialOfferEvent(
long SequenceNumber,
DateTimeOffset OccuredAt,
string Name,
object Content);

This concludes your implementation C# part of event subscriptions.

If you want to learn more, check out the book on Manning’s liveBook platform here.

--

--

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

Manning Publications

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