Exploring Dapr: Pub/Sub – Part 2: ASP.NET Core integration

In this “Exploring Dapr” series, I’m exploring some of its capabilities and sharing my thoughts on it.  Dapr is an event-driven portable runtime for building micro-services, for more information I refer to the documentation.  Be aware that Dapr is currently under constant evolution, so some of the described behavior or findings might not be accurate anymore in the future.

This blog post is based on Dapr version 0.3 and focuses on the ASP.NET core integration for its publish / subscribe messaging capabilities.  In this example, we will create a very basic application that hosts an Order API.  This API publishes a message on a topic and another part of the application is subscribing on it.  All code is published over here.

Create an ASP.NET Core Web API project

Open Visual Studio 2019 and perform the following tasks:

  • Create a new ASP.NET Core Web Application (C#) project

dapr-dotnet-01

  • Choose a meaningful name

dapr-dotnet-02

  • Choose the API template and make sure you select .NET Core 3.0.
    Disable HTTPS and enable Docker support.

dapr-dotnet-03

  • Delete the Weather Forecast controller and related files.

Publish an order to a topic

Let’s create an API operation that receives a dummy order and publishes it to a topic.

  • Add a Models folder, with a simplified Order class
    public class Order
    {
        public string id { get; set; }
        public string reference { get; set; }
        public string product { get; set; }
        public int quantity { get; set; }
        public float price { get; set; }
    }
  • Add a new API controller, named “OrderController” and remove its Route attribute.  Foresee a simple logging base.
    [ApiController]
    public class OrderController : ControllerBase
    {
        private readonly ILogger<OrderController> _logger;

        public OrderController(ILogger<OrderController> logger)
        {
            _logger = logger;
        }
    }
  • Add an API operation that will receive the order, validate it and publish it on the “ordertopic”.  Remark that 3500 is assumed to be the HTTP port that Dapr is listening on (default on Kubernetes).  For simplicity, the HttpClient is used.  Consider using the HttpClientFactory for production workloads.
    [HttpPost]
    [Route("api/order")]
    public async Task<IActionResult> ReceiveOrder([FromBody]Order order)
    {
        _logger.LogInformation($"Order with id {order.id} received!");

        //Validate order placeholder

        using (var httpClient = new HttpClient())
        {
             var result = await httpClient.PostAsync(
                  "http://localhost:3500/v1.0/publish/ordertopic",
                  new StringContent(JsonConvert.SerializeObject(order), Encoding.UTF8, "application/json")
             );

             _logger.LogInformation($"Order with id {order.id} published with status {result.StatusCode}!");
        }

        return Ok();
    }

 

Subscribe on the order topic

Let’s extend the application to subscribe on the order topic.

  • Install the Dapr.AspNetCore Nuget package, which is still in preview.  This package contains some useful middleware and attributes to simplify the subscribe part.

dapr-dotnet-04

  • Create an API operation that will subscribe on messages from the “ordertopic”.  This requires two things:
    • Add the Dapr Topic attribute to the operation
    • Add the Route attribute and makes sure it listens for a POST on /ordertopic
    [Topic("ordertopic")]
    [HttpPost]
    [Route("ordertopic")]
    public async Task<IActionResult> ProcessOrder([FromBody]Order order)
    {
        //Process order placeholder

        _logger.LogInformation($"Order with id {order.id} processed!");
        return Ok();
    }
  • In the Startup.cs file, add the Subscribe Handler.  This will automatically create an endpoint on dapr/subscribe that returns all topics that the application subscribes to (derived via the Topic attribute).  This is required for Dapr to know on what it has to subscribe.
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapSubscribeHandler();
        endpoints.MapControllers();
    });
  • In the Startup.cs file, register the CloudEvents middleware, which will unwrap the envelope for every incoming request with the application/cloudevents+json content-type.
   app.UseCloudEvents();
  • This will take the message that is part of the data property at root level:
{
   "id":"88d480fd-3891-4adb-99be-b3180a6a8df9",
   "source":"pubsub",
   "type":"com.dapr.event.sent",
   "specversion":"0.3",
   "datacontenttype":"application/json",
   "data":
   {
      "product":"laptop",
      "quantity":1,
      "price":1300,
      "id":"999",
      "reference":"tvh"
   }
}

Run the application locally

Let’s run the application locally, to see if everything works as expected.

  • Ensure you have all prerequisites configured correctly
  • Install and start Dapr in standalone mode, via the following command
dapr init
  • Start the ASP.NET application with the application listening on HTTP port 5000 (default for Kestrel) and the Dapr sidecar on 3500
dapr run --app-id pubsub --app-port 5000 --port 3500 dotnet run
  • At startup time, you’ll notice that Dapr performs a GET to see on what is has to subscribe
GET http://localhost:5000/dapr/subscribe
  • The application should return an array that includes the order topic
[
   "ordertopic"
]
  • Now you can trigger the Order API to test the application
POST http://localhost:5000/api/order
{
   "id" : "123",
   "reference" : "tvh",
   "product" : "laptop",
   "quantity" : 1,
   "price" : 1300.00
}
  • If we analyse the logs, you’ll notice the following important entries:
    1. The Order API gets invoked on api/order
    2. The order was parsed and received successfully
    3. The order was published on the ordertopic
    4. The Dapr sidecar subscribed on the order and invoked the app on /ordertopic
    5. The order was processed

dapr-dotnet-05

  • You can close the application through Ctrl + C

That was pretty slick!  But wait a minute…  we just did a publish / subscribe, but who the hell was the broker!?  At least, I didn’t specify any explicit messagebus component.  Well, in case you’re running Dapr standalone and when you didn’t specify any components, then Dapr will autogenerate component YAML files that interact with the local Redis container that ships with it.  Great local development experience!  In my case, two files were added:

  • redis.yaml: Redis state store running locally
  • redis_messagebus.yaml: Redis message bus running locally

dapr-dotnet-06

Debug the application

In case you want to debug your code inside Visual Studio 2019, while it’s running as a Dapr application, you just need attach to the right process.  Don’t look anymore for dotnet.exe (which used to be the way to do this), but just look for the name of your project, inside the list of running processes.

dapr-dotnet-07

Once you trigger the application, the breakpoints get hit and you can enjoy your normal debugging experience.

dapr-dotnet-08

In case you want to debug the Dapr Sidecar itself, I advise you to consult this page.  Dapr development is performed in Go.

Conclusion

It took me a while to figure out the ASP.NET core integration and how it worked, but once you get all the bits connected, you realize it’s actually very simple.  It’s great that you can remain within the comfort zone of ASP.NET Core API development, even while you’re actually dealing with the rather complex task of subscribing to messages.  I didn’t have to think about polling, error handling, authentication, reliability, etc…  Really slick programming experience!!!

Cheers
Toon

About me

Hi! I’m Toon Vanhoutte, a hands-on Azure architect – based in Belgium – with a big passion for teaching and helping people out. I’m happy to assist you during your Azure journey with high-quality advisory and I would love to teach you Azure’s possibilities via my tailored training courses.

Subscribe to the blog