Building a distributed pizza store in .NET with serverless Dapr APIs
Tags: programming, dapr, tutorial, microservices
Published: 2024-02-01 10:57:11

## Intro

When developers build distributed systems, they need to think about cross-cutting concerns such as security, observability, and resiliency. Developers also write a lot of glue code to orchestrate services, create long-running and durable workflows, and connect to external resources, such as message brokers and state stores. 

[Dapr](https://dapr.io/), the Distributed Application Runtime, is ideal for building distributed systems since it has built-in support for cross-cutting concerns, and it offers a suite of APIs for communication, state and workflow, that make developers more efficient.  According to the [State of Dapr Report](https://www.diagrid.io/blog/the-state-of-dapr-2023-report), 95% of Dapr developers claim that Dapr saves time, and 55% of the developers surveyed claimed it saved them more than 30% of development time.

The usage of Dapr is also increasing thanks to a growing number of platforms that support the Dapr APIs. Dapr is no longer limited to running as a sidecar in Kubernetes, as is described in [Dapr Deployment Models](https://www.diagrid.io/blog/dapr-deployment-models). In this article, [Diagrid Catalyst](https://www.diagrid.io/catalyst), a suite of serverless Dapr APIs, is used to orchestrate services with [Dapr workflow](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-overview/), and interact with message brokers and key-value stores.

Throughout this article, you’ll be using the following to configure a distributed system:

- Two Dapr services (.NET)
- Two serverless functions (JavaScript)
- A front-end (Vue)
- A key value state store
- A pub/sub message broker
- [Diagrid Catalyst](https://www.diagrid.io/catalyst) as the serverless API layer to connect everything

**TLDR:** Check the code on [GitHub](https://github.com/diagrid-labs/catalyst-pizza-demo) and join the [Catalyst early access program](https://pages.diagrid.io/catalyst-early-access-waitlist).

## Diagrid Pizza Store

The demo that is used in this article revolves around an online pizza store, where the user selects pizzas, orders them, and follows the progress of the order in realtime. It’s a simplified example where the durable workflow focuses on the inventory of ingredients, and communicating with the kitchen. The source code is [on GitHub](https://github.com/diagrid-labs/catalyst-pizza-demo).


![Pizza store front-end](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m7abjviwgbse7hnhc36g.gif)

## Architecture

The solution includes:

- Two [Dapr](http://dapr.io/) web services written in .NET, *PizzaOrderService* and *KitchenService*. In this demo, they’re running locally, but these can be hosted in any cloud that supports ASP.NET services.
- The website (based on Vue) and two serverless functions written in JavaScript, *getAblyToken* and *placeOrder*. These are hosted on [Vercel](https://vercel.com/).
- [Ably](https://ably.com/) as the realtime communication component between the *PizzaOrderService* and the website.
- A key/value store to manage inventory (managed by Diagrid).
- A pub/sub message broker to communicate between the *PizzaOrderService* and the *KitchenService* (managed by Diagrid).
- [Diagrid Catalyst](https://www.diagrid.io/catalyst) that offers serverless APIs for communication, state, and workflow powered by Dapr.

![Pizza store architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u1ornquwh30esbeu2k8d.png)

### Workflow

The *PizzaOrderService* contains a Dapr workflow that orchestrates the activities for checking the inventory and communicating with the *KitchenService.*

![Pizza store workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p1gdgcp6n4hiq9plzxrm.png)

*The workflow also contains activities for sending realtime messages to the website, these have been omitted from the workflow diagram to keep it concise.*

## Running the Pizza Store locally

### Prerequisites

The following services, tools, and frameworks are required for this demo:

- [Diagrid Catalyst account](https://www.diagrid.io/catalyst) ([sign up](https://pages.diagrid.io/catalyst-early-access-waitlist) for early access) and the [Diagrid CLI](https://docs.diagrid.io/catalyst/references/cli-reference/intro)
- [Vercel account (hobby)](https://vercel.com/signup) and the [Vercel CLI](https://vercel.com/docs/cli)
- [Ably account (free)](https://www.ably.com/signup)
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
- [Node 18](https://nodejs.org/en/download/)

All the code for the website, serverless functions, and back-end services is available in the [catalyst-pizza-demo](https://github.com/diagrid-labs/catalyst-pizza-demo) GitHub repository.  This repository contains a devcontainer configuration that has the following preinstalled: .NET 8, Node LTS, Vercel CLI, Diagrid CLI. 

You can use this devcontainer [locally in VSCode](https://code.visualstudio.com/docs/devcontainers/containers) (requires [Docker Desktop](https://www.docker.com/products/docker-desktop/)) or directly in [GitHub Codespaces](https://github.com/features/codespaces). The *npm install* and *dotnet build* commands described in this article can be skipped if the devcontainer is used.

### GitHub

1. [Fork](https://github.com/diagrid-labs/catalyst-pizza-demo/fork) the [catalyst-pizza-demo](https://github.com/diagrid-labs/catalyst-pizza-demo) and clone it locally, or use GitHub Codespaces (recommended).

### Ably

Ably is used as the serverless realtime messaging component. There is a default Ably app when you sign up for an account that can be used for this demo. Alternatively, you can create a new Ably app.

1. Log into the [Ably portal](https://ably.com/accounts/).
2. Using the Ably portal: copy the [Root API key](https://ably.com/docs/ids-and-keys#api-key) from the Ably app. This will be copied later as an environment variable for both Vercel and Diagrid.

### Vercel

The Vue-based [front-end](https://github.com/diagrid-labs/catalyst-pizza-demo/tree/main/front-end/src) and two [JavaScript functions](https://github.com/diagrid-labs/catalyst-pizza-demo/tree/main/front-end/api) are hosted on Vercel. The [Vercel CLI](https://vercel.com/docs/cli) is used to configure and run these resources locally.

1. Open a terminal in the root of the repository and login with the Vercel CLI:
    
    ```bash
    vercel login
    ```
    
2. Go to the *front-end* folder and run:
    
    ```bash
    npm install
    ```
    
3. Go back to the root of the repository and set up the Vercel project by running:
    
    ```bash
    vercel
    ```
    
    Follow the CLI prompts, and select the following options:
  - Setup and deploy: `Y` 
  - Scope: `<account name>`
  - Link to existing project: `N`
  - What's your project's name? `catalyst-pizza-project`
  - In which directory is your code located? `./front-end`
  - Want to modify these settings? [y/N] `n` 
  - Wait for the deployment to complete.

4. An environment variable is used in the *getAblyToken* function to generate a token for the website to communicate with the Ably realtime service. Add the *Ably API token* variable by running `vercel env add`:
    - Variable name: `ABLY_API_KEY`
    - Variable value: *Use the Ably API key obtained from the Ably portal earlier*
    - Select `Development` as the environment.
5. Another environment variable is used in the *placeOrder* function to send a request to the *PizzaOrderService* that will start the workflow. Add the *WORKFLOW_URL* variable by running `vercel env add`:
    - Variable name: `WORKFLOW_URL`
    - Variable value: `http://localhost:5064/workflow/orderReceived`
    - Select `Development` as the environment.
6. Run `vercel pull` to pull the configuration from Vercel to your local environment.
7. Run `vercel build` to build the website and the serverless functions to ensure there are no errors.

### Diagrid **Catalyst**

Diagrid Catalyst provides serverless Dapr APIs that enables developers to quickly build distributed applications with workflow, pub/sub messaging, service invocation, and state management capabilities. Diagrid also provides managed infrastructure for storing data in a key/value store, pub/sub messaging, and workflow, which are all used in this solution.

The [Diagrid CLI](https://docs.diagrid.io/catalyst/references/cli-reference/intro) is used to configure the resources and run the .NET services locally.

1. Open another terminal in the root of the repository and use the Diagrid CLI to log in to Diagrid:
    
    ```bash
    diagrid login
    ```
    
2. Create a new Catalyst project named *catalyst-pizza-project* and use the Diagrid managed pub/sub broker & KV store, and enable the managed workflow API:
    
    ```bash
    diagrid project create catalyst-pizza-project --deploy-managed-pubsub --deploy-managed-kv --enable-managed-workflow --wait
    ```
    
3. To set this project as the default in the CLI run:
    
    ```bash
    diagrid project use catalyst-pizza-project
    ```
    
4. Create a new App ID for the *PizzaOrderService*:
    
    ```bash
    diagrid appid create pizzaorderservice
    ```
    
5. Create a new App ID for the *KitchenService*:
    
    ```bash
    diagrid appid create kitchenservice
    ```
    
6. Before continuing, check the App ID status to make sure they are ready:
    
    ```bash
    diagrid appid list
    ```
    
7. Create a pub/sub subscription for the *KitchenService* to receive messages from the *PizzaOrderService*:
    
    ```bash
    diagrid subscription create pizzaorderssub --connection pubsub --topic pizza-orders --route /prepare --scopes kitchenservice
    ```
    
8. Create a pub/sub subscription for the *PizzaOrderService* to receive messages from the *KitchenService*:
    
    ```bash
    diagrid subscription create preparedorderssub --connection pubsub --topic prepared-orders --route /workflow/orderPrepared --scopes pizzaorderservice
    ```
    
9. Verify that creation of the subscriptions is completed:
    
    ```bash
    diagrid subscription list
    ```
    
10. Run `diagrid dev scaffold` to create a new local dev environment file. This creates a yaml file, named *dev-<PROJECT NAME>.yaml* with the following content:
    
    ```yaml
    project: catalyst-pizza-project
    apps:
    - appId: kitchenservice
    appPort: 0
    env:
    	DAPR_API_TOKEN: diagrid://<dapr_api_token>
    	DAPR_APP_ID: kitchenservice
    	DAPR_GRPC_ENDPOINT: https://<grpc_endpoint>
    	DAPR_HTTP_ENDPOINT: https://<http_endpoint>
    workDir: kitchenservice
    command: []
    - appId: pizzaorderservice
    appPort: 0
    env:
    	DAPR_API_TOKEN: diagrid://<dapr_api_token>
    	DAPR_APP_ID: pizzaorderservice
    	DAPR_GRPC_ENDPOINT: https://<grpc_endpoint>
    	DAPR_HTTP_ENDPOINT: https://<http_endpoint>
    workDir: pizzaorderservice
    command: []
    appLogDestination: ""
    ```
    
    This file is gitignored since it contains secrets from Diagrid Catalyst. The *appPort*, *command*, and *workDir* attributes still need to be updated as follows: 
    
  - Update the *appPort* for the *kitchenservice* to `5066`.
  - Update the *appPort* for the *pizzaorderservice* to `5064`.
  - Update the *command* arguments to `["dotnet", "run"]` for both apps.
  - Update the *workDir* argument to point to `back-end/KitchenService` and `back-end/PizzaOrderService` respectively.
  - Update the *appLogDestination* to `console`.
  - Add an `ABLY_API_KEY` environment variable for the *pizzaorderservice* appId and set the value to the Ably API key obtained from the Ably portal.
  - Save the changes to the file.

**Update the key/value store to allow shared state**

The two .NET services share the same state in the key/value store to manage inventory and orders. Since this is not the default usage of state stores and services when using Dapr, an attribute needs to be set to enable this. The default behavior is that a key is prefixed with the app ID of the service that is using it. In this case, however, the *keyPrefix* is set to *name* to make sure both services use the same keys.

1. Run the following command to update the managed key/value store:
    
    ```bash
    diagrid connection apply -f ./infra/kv.yml
    ```
    
    This will upload the *kv.yml* file to Diagrid and update the configuration of the *kvstore* connection so *keyPrefix* is set to *name*.

### Inspect the DaprClient configuration

The two .NET services use the Dapr .NET client SDK for workflow, state management and pub/sub messaging. The *DaprClient* is configured to use the endpoints provided by Diagrid Catalyst in the *[Program.cs](https://github.com/diagrid-labs/catalyst-pizza-demo/blob/main/back-end/PizzaOrderService/Program.cs)* file:

```csharp
var apiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN");
var grpcEndpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT");
var httpEndpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT");

builder.Services.AddDaprClient(options => {
    options.UseDaprApiToken(apiToken);
    options.UseGrpcEndpoint(grpcEndpoint);
    options.UseHttpEndpoint(httpEndpoint);
});
```

To use service invocation with the HTTP API, the *HttpClient* is configured with the *DAPR_APP_ID* and *DAPR_API_TOKEN* environment variables:

```csharp
var appId = Environment.GetEnvironmentVariable("DAPR_APP_ID");
var apiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN");

builder.Services.AddHttpClient(
    "daprEndpoint",
    client => {
        client.BaseAddress = new Uri(httpEndpoint);
        client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("dapr-app-id", appId);
        client.DefaultRequestHeaders.Add("dapr-api-token", apiToken);
    });
```

### Running & testing the solution

1. Open a terminal in the root of the repository.
2. To restore and build the .NET projects run:
    
    ```bash
    dotnet build ./back-end/PizzaOrderService
    dotnet build ./back-end/KitchenService
    ```
    
3. Run `diagrid dev start` to start the *PizzaOrderService* and the *KitchenService*.
4. Using another terminal in the root of the repository, run `vercel dev` to start the website and the serverless functions locally.
5. Navigate to the URL provided by the Vercel CLI to view the website.
6. Select some pizzas, place an order, and watch the progress of the workflow in realtime.

### **Using the Catalyst API explorer**

You can use the API explorer in the Catalyst web UI to interact with the APIs. If you want to retrieve the order item from the key/value store that has just been processed, follow these steps:

1. Open the developer tools console of the browser that is running the demo (a pizza order must be started or completed).
2. The order is logged to the console as a JSON object. Copy the *OrderId* property value.
  ![Console order](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/82q8tifruveh69ym3ek1.png)
3. Navigate to the [Catalyst web UI](https://catalyst.diagrid.io/). Ensure you're in the *catalyst-pizza-project* project.
  ![Catalyst API explorer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g58vyhxqrhxj63rntjp0.png)
4. Select *App IDs* in and click on the *[API explorer](https://catalyst.diagrid.io/app-ids/api-explorer/state-api/)* tab.
    - Select the *State API*.
    - Select the *pizzaorderservice* as the app ID.
    - Select *GET* as the API operation.
    - Select *kvstore* as the state connection.
    - Enter *Order-<ORDER-ID>* as the key, where *<ORDER-ID>* is substituted with the value copied from the developer tools console.
5. Click *Send*. The response should show the state of the order item.

## What’s next?

Congratulations! You’ve now used the Diagrid Catalyst serverless Dapr APIs for workflow, pub/sub messaging, service invocation, and state management. The ability to use these APIs from anywhere, without the overhead of managing Kubernetes clusters, brings great flexibility to developers on any platform to build distributed applications. The Dapr applications used in this demo can be hosted on any cloud. The demo uses the Diagrid managed infrastructure for key/value storage and pub/sub messaging, but these can be swapped out for other cloud-based resources, similarly to switching open-source Dapr components. You can extend this demo with an alternative pub/sub broker or state store by configuring other [infrastructure connections](https://docs.diagrid.io/catalyst/concepts/connections).

Any questions or comments about this demo? Join the [Diagrid Community on Discourse](https://community.diagrid.io/invites/fAUrdyBbie) and post a message in the *Catalyst* category. Have you made something with Catalyst? Post a message in the *Built with Catalyst* category, we love to see your creations!