Introduction

The Request-Response pattern is robust yet has limitations, leading to the emergence of patterns like Publish-Subscribe to address some drawbacks. However, the infrastructure complexities of a Pub-Sub messaging system can be expensive to implement and maintain.

This article delves into an intermediate solution: Webhooks. It discusses the problems they solve and guides their implementation in .NET.

Challenges arise with the Request-Response pattern

The synchronous nature of the Request-Response pattern poses a challenge. Consider a scenario: you wish to stay updated about newly arrived items in stock. Traditionally, you’d query the Warehouse periodically to check for new arrivals.

However, what if immediate notifications about new items are crucial?

The typical Request-Response method necessitates a polling technique – either regular or long polling. Both methods involve frequent server queries, straining resources and lacking scalability.

 

To address this, we require a system where clients can subscribe to receive updates on a topic, enabling the server to push these updates without clients constantly requesting them.

What Is a Webhook?

Webhooks occupy a middle ground between Request-Response communication and a comprehensive Publish-Subscribe system.

They address asynchronous communication issues without the intricate infrastructure of a broker system. Essentially, they operate as a network-based callback mechanism.

Through webhooks, a client can subscribe to a specific topic, enabling the server to seamlessly notify the client about any updates related to that topic.

The Building Blocks

A webhook system comprises fundamental components:

1. Subscription

This model holds crucial information to notify a single client subscribed to a specific topic. It includes the topic itself and the callback URI.

2. Topic

Represents the subject of the subscription, often reflecting a domain event, like the creation of a new item.

3. Callback

The callback URI is where the server sends the payload. The client must set up an API endpoint at this URI to receive and process the payload.

4. Payload

A concise message containing event information, typically a serialized JSON representation of an event.

5. Trigger

This initiates the webhook. From a domain perspective, it’s an event related to a particular topic. Technically, it could range from a simple in-process method call to an event handler consuming a message from another part of the system.

Creating a webhook server involves establishing a singular API endpoint

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton();
var app = builder.Build();
app.MapPost("/subscribe", (WebhookService ws, Subscription sub)
=> ws.Subscribe(sub));
app.Run();

This endpoint will communicate with our WebhookService, where the core business logic resides.

Within the service, we define the Subscription model, comprising a topic and a callback.

We implement a Subscribe method responsible for handling subscriptions.

To showcase functionality, we maintain a list of subscriptions in memory, though typically this information would be stored in a database for scalability and persistence.

public record Subscription(string Topic, string Callback);
public class WebhookService
{
privte readonly List _subscriptions = new();
public void Subscribe(Subscription subscription)
{
_subscriptions.Add(subscription);
}
}

Adding the Trigger

Next, we aim to integrate the trigger mechanism. To facilitate testing, we’ll introduce an additional API endpoint responsible for invoking our PublishMessage method within the WebhookService.

app.MapPost("/publish", async (WebhookService ws, PublishRequest req)
=> await ws.PublishMessage(req.Topic, req.Message));
record PublishRequest(string Topic, object Message);

Implementing a Webhook Client

Our Webhook client intends to subscribe to the item.new topic. It will furnish a callback URI and implement an endpoint that performs a basic action, such as logging to the console.

We will run the server on http://localhost:5003 and the client on http://localhost:5004.

const string server = "http://localhost:5003";
const string callback = "http://localhost:5004/wh/item/new";
const string topic = "item.new";
var client = new HttpClient();
Console.WriteLine($"Subscribing to topic {topic} with callback {callback}");
await client.PostAsJsonAsync(server + "/subscribe", new { topic, callback });
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLogging();
var app = builder.Build();
app.MapPost("/wh/item/new", (object payload, ILogger logger) =>
{
logger.LogInformation("Received payload: {payload}", payload);
});
app.Run();

We’ve established a straightforward webhook server and client, and now it’s time to test the setup.

To trigger the endpoint, you can use PowerShell or, if you’re on macOS or Linux, utilize curl.

In the request body, specify the topic as “item.new.” Include basic item details in the message, such as a name and price. This information will be transmitted to the endpoint.

$body = @{
Topic = "item.new"
Message = @{
Name = "Some Item"
Price = "2.55"
}
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:5003/publish" -Method POST -Body $body -ContentType "application/json"

If everything is set up correctly, you should see this in the Client’s console:

info: Program[0]
Received payload: {"Price":"2.55","Name":"Some Item"}

Conclusion

In this comprehensive guide, we’ve navigated through the core concepts necessary to comprehend and deploy a webhook system.

From theory to practice, we’ve constructed a functioning proof of concept that demonstrates the implementation steps.

While this guide provides a foundational understanding, in a production environment, it’s crucial to delve into areas like retry policies and security considerations. These aspects, while vital, extend beyond the scope of this guide.

At Cogtix, we strive to equip IT software development teams with the knowledge and tools necessary to implement efficient and robust systems, and this guide serves as a starting point toward that goal.