Implementing Dapr Pub/Sub functionality to ASP.NET Core Web APIs
Will Velida
Posted on August 16, 2023
In event-driven architectures, communicating between different microservices via messages can be enabled using Publish and Subscribe functionality (Pub/Sub for short).
The publisher (or producer) writes messages to a message topic (or queue), while a subscriber will subscribe to that particular topic and receives the message. Both the publisher and subscriber are unaware of which application either produced or subscribed to the topic. This pattern is useful when we want to send messages to different services, while ensuring that they are decoupled from each other.
image taken from https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-overview/
In this article, we'll discuss how Pub/Sub works in Dapr, and how we can implement Pub/Sub functionality in a ASP.NET Core Web API. We'll then configure our Pub/Sub component and wire it up to our API to see it in action.
What is Pub/Sub in Dapr?
Dapr has a Pub/Sub API that you can use to implement Pub/Sub capabilities to your applications. It's platform agnostic, meaning that you can a variety of message brokers to send and receive messages from. We can configure our message broker as a Dapr Pub/Sub component at runtime, removing the dependency from our service and making it more flexible to changes (should you need, or even want to, change your message broker).
The API offers at-least-once message guarantees, meaning that Dapr ensures that the message will be delivered at least once to every subscriber. In the event that a message fails to deliver, or your application fails, Dapr will attempt to redeliver the message until it's successful.
When we use Pub/Sub in Dapr, our service (In our case, our publisher service), will make a network call to a Pub/Sub building block API. This building block will make a call to our Pub/Sub component (our configured message broker). To receive messages on a topic, Dapr subscribes to the Pub/Sub component with a topic and then delivers messages to a particular endpoint when messages arrive.
In this article, we'll build two APIs. One that will act as our publisher which will send messages to our message broker (In this example, I'll be using Azure Service Bus), and the other will act as our subscriber, which will receive messages from our Service Bus topic:
Dapr Pub/Sub has a variety of features available that simplifies Pub/Sub capabilities in your application, so I'd recommend taking a look at the Dapr documentation on Pub/Sub if you want to gain a deeper understanding.
Configuring Dapr in our Web API
To see Dapr Pub/Sub in action, we'll build two ASP.NET Core Web APIs: One to handle the publishing of an Bookshop order message, another to subscribe to the topic that our orders will be sent to so they can receive the message.
To see the full code for this project, please check out this repository on my GitHub.
To work with Dapr in an ASP.NET Core Web, we'll need to install the Dapr.AspNetCore
package. For this tutorial, we'll need to install this package in both our API's. To do this, we can run the following NET CLI command in our Web API projects:
dotnet add package Dapr.AspNetCore
Alternatively, you can use the NuGet Package Manager in Visual Studio to install it.
This package will allow you to interact with Dapr applications through the Dapr Client and build routes and controllers in your ASP.NET applications using Dapr.
Implementing our Pub/Sub logic
Now that the Dapr package has been installed in both our APIs, we can start to build our publisher API. For this application, we're going to keep our Order model simple like so:
namespace Bookshop.Common
{
public class Order
{
public string OrderId { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public decimal Price { get; set; }
}
}
For our publisher, we can create the following controller:
using Bookshop.Common;
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
namespace Bookshop.Publisher.Controllers
{
[Route("api/orders")]
[ApiController]
public class OrderPublisherController : ControllerBase
{
private readonly DaprClient _daprClient;
private readonly ILogger<OrderPublisherController> _logger;
public OrderPublisherController(DaprClient daprClient, ILogger<OrderPublisherController> logger)
{
_daprClient = daprClient;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] Order order)
{
if (order is not null)
{
_logger.LogInformation($"Publishing order ID {order.OrderId}");
await _daprClient.PublishEventAsync("dapr-pubsub", "orderstopic", order);
return Ok(order);
}
return BadRequest();
}
}
}
Let's break this down.
We inject our DaprClient
into the controller since we'll be using it to publish our event. In this controller, we have a single method called CreateOrder
which will be invoked when we make a POST request. We'll need to pass an Order request payload to the endpoint, which will invoke the PublishEventAsync
method.
In this method, we pass three parameters:
-
dapr-pubsub
- This will be the name of the Pub/Sub component that we will define later. -
orderstopic
- This is the name of our topic that we will send Order messages to. -
order
- This is the payload that we'll send to ourorderstopic
.
In our Bookshop.Publisher Program.cs
file, we need to add the DaprClient service to our service container like so:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddDaprClient();
builder.Services.AddControllers();
// Rest of file
To receive messages from our orderstopic
, we can create a controller that will act as our subscriber:
using Bookshop.Common;
using Microsoft.AspNetCore.Mvc;
namespace Bookshop.Subscriber.Controllers
{
[Route("api/orders")]
[ApiController]
public class OrderSubscriberController : ControllerBase
{
private readonly ILogger<OrderSubscriberController> _logger;
public OrderSubscriberController(ILogger<OrderSubscriberController> logger)
{
_logger = logger;
}
[Dapr.Topic("dapr-pubsub", "orderstopic")]
[HttpPost("orderreceived")]
public async Task<IActionResult> OrderReceived([FromBody] Order order)
{
if (order is not null)
{
_logger.LogInformation($"Received Order ID: {order.OrderId}: {order.Title} - {order.Author} - {order.Price}");
return Ok();
}
return BadRequest();
}
}
}
Again, let's break this down.
We don't need to inject a DaprClient
into this controller. Instead, we annotate our OrderReceived
method with the Dapr.Topic
attribute. This attribute accepts two arguments:
- The name of our Pub/Sub component that this subscriber will target. In this case, our component will be called
dapr-pub
, which we will define later. - The name of the topic that to subscribe to, which is called
orderstopic
.
The action method will expect to receive an Order
object and once the message is received, we will log out the order that has been received and return a 200
response.
In our subscriber Program.cs
file, we need to add the following:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers().AddDapr();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseAuthorization();
app.UseCloudEvents();
app.MapControllers();
app.MapSubscribeHandler();
app.Run();
Let's break this down,
- We integrate Dapr into the MVC pipeline in the line
builder.Services.AddControllers().AddDapr()
. - On line
app.UseCloudEvents()
, this will add CloudEvents middleware into the ASP.NET Core middleware pipeline. This unwraps requests that use the CloudEvents structured format, so the receiving method can read the event payload that is sent directly. - On line
app.MapSubscribeHandler()
, this will make the endpointhttp://localhost:<your-app-port>/dapr/subscribe
available for the consumer so it responses and returns the available subscriptions. When the endpoint is called, it finds all the WebAPI actions decorated with theDapr.Topic
attribute and will tell Dapr to create subscriptions for them.
Working with Dapr Pub/Sub components
Now that our application code is finished, we'll need to set up our Pub/Sub component. For my message broker, I'm going to be using Azure Service Bus. To define our Pub/Sub component to use Azure Service Bus, we can write the following YAML:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: dapr-pubsub
spec:
type: pubsub.azure.servicebus
version: v1
metadata:
- name: connectionString
value: "<YOUR-SERVICE-BUS-CONNECTION-STRING>"
- name: consumerID
value: "order-processor-subscription"
To define which message broker we will use for Pub/Sub capabilities, we define this using the type
field. So for Azure Service Bus, this will be of type pubsub.azure.servicebus
. In the metadata
section, we just need to define our connectionString
and the consumerID
. Now for this example, I'm using connection strings purely for local development. In production scenarios, please use Azure AD to authenticate to your Service Bus namespace for more granular control. You can also use Dapr secret stores to store sensitive values.
For our consumerID
, I've created a subscription for my Service Bus topic called order-processor-subscription
. The value that we give our consumerID
in our Pub/Sub component should match the name of your subscription.
The power of the Dapr framework lies in its portability, meaning that you can plug in different message brokers without having to change your application code. Check out this guide on how to configure Pub/Sub components in Dapr and this reference to see all the supported message brokers available to you for your Dapr applications.
Testing our API
With our application logic and Pub/Sub component defined, we can now test sending and receiving messages via Dapr. To do this, we'll need to open two separate command lines (one for our publisher, one for our subscriber). In each command line, run the following commands:
# Run our Publisher API
dapr run --app-id bookshop-publisher --app-port <app-port-defined-in-launchSettings.json-file> --dapr-http-port 3500 --resources-path ..\..\..\components\ dotnet run
# Run our Subscriber API
dapr run --app-id bookshop-subscriber --app-port <app-port-defined-in-launchSettings.json-file> --dapr-http-port 3502 --resources-path ..\..\..\components\ -- dotnet run
Again, let's break these commands down:
- The
--app-id
parameter sets the id for our applications. - For our
--app-port
, we will use the ports defined in our APIslaunchSettings.json
file. - We need to give both our APIs different
--dapr-http-ports
, so I've used3500
for our publisher and3502
for our subscriber. - I've added my Pub/Sub component into a
components
folder, so I've used the--resources-path
to point to that folder when running our application. - We then run both our publisher and subscriber with
dotnet run
.
To test our APIs, we start by sending a POST
request to our publisher endpoint so it will send a message to our topic. For this, I'm going to use Postman. In Postman, we can do this like so:
Here, we make a POST
request to our http://localhost:5105/api/orders
endpoint, and send a JSON payload for our order. In our controller, we
Since we're running our Subscriber API at the same time, we should see in our Subscriber terminal that the message has been received.
Conclusion
In this article, we talked about Pub/Sub in Dapr and how we can use it to enable Pub/Sub capabilities in our distributed applications. We then learnt how to configure a publisher and subscriber in ASP.NET Core Web API projects, how to configure our pub/sub component and how we can test our publisher and subscriber projects locally.
If you have any questions on the above, feel free to reach out to me on twitter (or X, whichever suits you) @willvelida
Until next time, Happy coding! 🤓🖥️
Posted on August 16, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024