Skip to content

Module 3 - Dapr Integration with ACA

Module Duration

60 minutes

Objective

In this module, we will accomplish two objectives:

  1. Introduce the Distributed Application Runtime (Dapr).
  2. Decouple ACA Web - Frontend from ACA API - Backend locally via Dapr.

Module Sections

  • From the VS Code Terminal tab, open developer command prompt or PowerShell terminal in the project folder TasksTracker.ContainerApps (root):

    cd ~\TasksTracker.ContainerApps
    
  • Restore the previously-stored variables by executing the local script. The output informs you how many variables have been set.

    .\Variables.ps1
    

1. Introduce Dapr

1.1 Benefits of Integrating Dapr in Azure Container Apps

The Tasks Tracker microservice application is composed of multiple microservices. Function calls are spread across the network. To support the distributed nature of microservices, we need to account for failures, retries, and timeouts. While Azure Container Apps features the building blocks for running microservices, using the Distributed Application Runtime (Dapr) provides an even richer microservices programming model.

Dapr includes features like service discovery, pub/sub, service-to-service invocation with mutual TLS, retries, state store management, and more. Here is a good link which touches on some of the benefits of the Dapr service invocation building block which we will be building upon in this module. Because the calls will flow through container sidecars, Dapr can inject some useful cross-cutting behaviors that are meaningfully abstracted from our application containers.

Although we won't tap into all these benefits in this workshop, it's worth noting that you will probably need to rely on these features in production:

  • Automatically retry calls upon failure.
  • Make calls between services secured with mutual authentication (mTLS), including automatic certificate rollover.
  • Control what operations clients can perform using access control policies.
  • Capture traces and metrics for all calls between services to provide insights and diagnostics.

1.2 Configure Dapr on a Local Development Machine

In order to run applications using Dapr, we need to install and initialize Dapr CLI locally. The official documentation is quite clear, and we can follow the steps needed to install Dapr and then Initialize it.

2. Decouple ACA Web - Frontend from ACA Web - Frontend Locally via Dapr

2.1 Configure Local Variables

You are now ready to run the applications locally using the Dapr sidecar in a self-hosted mode. The Dapr VS Code extension will allow you to run, debug, and interact with Dapr-enabled applications.

  • Let's start by capturing the UI and API localhost ports:

    $API_APP_PORT=<web api https port in Properties->launchSettings.json (e.g. 7112)>
    $UI_APP_PORT=<web ui https port in Properties->launchSettings.json (e.g. 7000)>
    

    Note

    Remember to replace the placeholders with your own values based on image below. Remember to use https port number for the Web API application.

    app-port

  • Now that we know the UI_APP_PORT, we can also declare the local frontend UI URL:

    $FRONTEND_UI_BASE_URL_LOCAL="https://localhost:$UI_APP_PORT"
    
  • Execute the Set-Variables.ps1 in the root to update the variables.ps1 file with all current variables. The output of the script will inform you how many variables are written out.

2.2 Test ACA API - Backend Locally

1
2
3
4
5
6
7
8
cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api

dapr run `
--app-id tasksmanager-backend-api `
--app-port $API_APP_PORT `
--dapr-http-port 3500 `
--app-ssl `
-- dotnet run --launch-profile https
Want to learn more about Dapr run command above?

When using Dapr run command you are running a Dapr process as a sidecar next to the Web API application. The properties you have configured are as follows:

  • app-id: The unique identifier of the application. Used for service discovery, state encapsulation, and the pub/sub consumer identifier.
  • app-port: This parameter tells Dapr which port your application is listening on. You can get the app port from Properties->launchSettings.json file in the Web API Project as shown in the image above. Make sure you use the https port listed within the Properties->launchSettings.json as we are using the --app-ssl when running the Dapr cli locally. Don't use the port inside the DockerFile. The DockerFile port will come in handy when you deploy to ACA at which point the application would be running inside a container.
  • dapr-http-port: The HTTP port for Dapr to listen on.
  • app-ssl: Sets the URI scheme of the app to https and attempts an SSL connection.

For a full list of properties, you can check this link.

If all is working as expected, you should receive an output similar to the one below where your app logs and Dapr logs will appear on the same PowerShell terminal:

dapr-logs

Now to test invoking the Web API using Dapr sidecar, you can issue an GET request to the following URL: http://localhost:3500/v1.0/invoke/tasksmanager-backend-api/method/api/tasks?createdBy=tjoudeh@bitoftech.net

Want to learn more about what is happening here?

What happened here is that Dapr exposes its HTTP and gRPC APIs as a sidecar process which can access our Backend Web API. We didn't do any changes to the application code to include any Dapr runtime code. We also ensured separation of the application logic for improved supportability.

Looking back at the HTTP GET request, we can break it as follows:

  • /v1.0/invoke Endpoint: is the Dapr feature identifier for the "Service to Service invocation" building block. This building block enables applications to communicate with each other through well-known endpoints in the form of http or gRPC messages. Dapr provides an endpoint that acts as a combination of a reverse proxy with built-in service discovery while leveraging built-in distributed tracing and error handling.
  • 3500: the HTTP port that Dapr is listening on.
  • tasksmanager-backend-api: is the Dapr application unique identifier.
  • method: reserved word when using invoke endpoint.
  • api/tasks?createdBy=tjoudeh@bitoftech.net: the path of the action method that needs to be invoked in the webapi service.

Another example is that we want to create a new task by invoking the POST operation, we need to issue the below POST request:

1
2
3
4
5
6
7
8
9
POST /v1.0/invoke/tasksmanager-backend-api/method/api/tasks/ HTTP/1.1
Host: localhost:3500
Content-Type: application/json
{
        "taskName": "Task number: 51",
        "taskCreatedBy": "tjoudeh@bitoftech.net",
        "taskDueDate": "2022-08-31T09:33:35.9256429Z",
        "taskAssignedTo": "assignee51@mail.com"
}

2.3 Configure ACA Web - Frontend Locally

  • Next, we will be using Dapr SDK in the frontend Web App to invoke Backend API services, The Dapr .NET SDK provides .NET developers with an intuitive and language-specific way to interact with Dapr.

    The SDK offers developers three ways of making remote service invocation calls:

    1. Invoke HTTP services using HttpClient
    2. Invoke HTTP services using DaprClient
    3. Invoke gRPC services using DaprClient

    We will be using the second approach in this workshop (HTTP services using DaprClient), but it is worth spending some time explaining the first approach (Invoke HTTP services using HttpClient). We will go over the first approach briefly and then discuss the second in details.

  • Install Dapr SDK for .NET Core in the Frontend Web APP, so we can use the service discovery and service invocation offered by Dapr Sidecar. To do so, add below nuget package to the project.

    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Dapr.AspNetCore" Version="1.14.0" />
      </ItemGroup>
    
    </Project>
    
    <Project Sdk="Microsoft.NET.Sdk.Web">
    
      <PropertyGroup>
        <TargetFramework>net9.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Dapr.AspNetCore" Version="1.14.0" />
      </ItemGroup>
    
    </Project>
    
    • Next, open the file Programs.cs of the Frontend Web App and register the DaprClient as the highlighted below.
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    
    builder.Services.AddDaprClient();
    
    builder.Services.AddHttpClient("BackEndApiExternal", httpClient =>
    {
        var backendApiBaseUrlExternalHttp = builder.Configuration.GetValue<string>("BackendApiConfig:BaseUrlExternalHttp");
    
        if (!string.IsNullOrEmpty(backendApiBaseUrlExternalHttp)) {
            httpClient.BaseAddress = new Uri(backendApiBaseUrlExternalHttp);
        } else {
            throw new("BackendApiConfig:BaseUrlExternalHttp is not defined in App Settings.");
        }
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapRazorPages();
    
    app.Run();
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    
    builder.Services.AddDaprClient();
    
    builder.Services.AddHttpClient("BackEndApiExternal", httpClient =>
    {
        var backendApiBaseUrlExternalHttp = builder.Configuration.GetValue<string>("BackendApiConfig:BaseUrlExternalHttp");
    
        if (!string.IsNullOrEmpty(backendApiBaseUrlExternalHttp)) {
            httpClient.BaseAddress = new Uri(backendApiBaseUrlExternalHttp);
        } else {
            throw new("BackendApiConfig:BaseUrlExternalHttp is not defined in App Settings.");
        }
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapStaticAssets();
    app.MapRazorPages()
       .WithStaticAssets();
    
    app.Run();
    
  • Now, we will inject the DaprClient into the .cshtml pages to use the method InvokeMethodAsync (second approach). Update files under folder Pages\Tasks and use the code below for different files.

    using Dapr.Client;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models;
    
    namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks
    {
        public class IndexModel : PageModel
        {
            private readonly IHttpClientFactory _httpClientFactory;
            private readonly DaprClient _daprClient;
            public List<TaskModel>? TasksList { get; set; }
    
            [BindProperty]
            public string? TasksCreatedBy { get; set; }
    
            public IndexModel(IHttpClientFactory httpClientFactory, DaprClient daprClient)
            {
                _httpClientFactory = httpClientFactory;
                _daprClient = daprClient;
            }
    
            public async Task<IActionResult> OnGetAsync()
            {
                TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"];
    
                if (!String.IsNullOrEmpty(TasksCreatedBy)) {
                    // Invoke via internal URL (Not Dapr)
                    //var httpClient = _httpClientFactory.CreateClient("BackEndApiExternal");
                    //TasksList = await httpClient.GetFromJsonAsync<List<TaskModel>>($"api/tasks?createdBy={TasksCreatedBy}");
    
                    // Invoke via Dapr SideCar URL
                    //var port = 3500;//Environment.GetEnvironmentVariable("DAPR_HTTP_PORT");
                    //HttpClient client = new HttpClient();
                    //var result = await client.GetFromJsonAsync<List<TaskModel>>($"http://localhost:{port}/v1.0/invoke/tasksmanager-backend-api/method/api/tasks?createdBy={TasksCreatedBy}");
                    //TasksList = result;
    
                    // Invoke via DaprSDK (Invoke HTTP services using HttpClient) --> Use Dapr Appi ID (Option 1)
                    //var daprHttpClient = DaprClient.CreateInvokeHttpClient(appId: "tasksmanager-backend-api"); 
                    //TasksList = await daprHttpClient.GetFromJsonAsync<List<TaskModel>>($"api/tasks?createdBy={TasksCreatedBy}");
    
                    // Invoke via DaprSDK (Invoke HTTP services using HttpClient) --> Specify Custom Port (Option 2)
                    //var daprHttpClient = DaprClient.CreateInvokeHttpClient(daprEndpoint: "http://localhost:3500"); 
                    //TasksList = await daprHttpClient.GetFromJsonAsync<List<TaskModel>>($"http://tasksmanager-backend-api/api/tasks?createdBy={TasksCreatedBy}");
    
                    // Invoke via DaprSDK (Invoke HTTP services using DaprClient)
                    TasksList = await _daprClient.InvokeMethodAsync<List<TaskModel>>(HttpMethod.Get, "tasksmanager-backend-api", $"api/tasks?createdBy={TasksCreatedBy}");
                    return Page();
                } else {
                    return RedirectToPage("../Index");
                }
            }
    
            public async Task<IActionResult> OnPostDeleteAsync(Guid id)
            {
                // Dapr SideCar Invocation
                await _daprClient.InvokeMethodAsync(HttpMethod.Delete, "tasksmanager-backend-api", $"api/tasks/{id}");
    
                return RedirectToPage();
            }
    
            public async Task<IActionResult> OnPostCompleteAsync(Guid id)
            {
                // Dapr SideCar Invocation
                await _daprClient.InvokeMethodAsync(HttpMethod.Put, "tasksmanager-backend-api", $"api/tasks/{id}/markcomplete");
    
                return RedirectToPage();
            }
        }
    }
    
    using Dapr.Client;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models;
    
    namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks
    {
        public class CreateModel : PageModel
        {
            private readonly IHttpClientFactory _httpClientFactory;
            private readonly DaprClient _daprClient;
    
            public CreateModel(IHttpClientFactory httpClientFactory, DaprClient daprClient)
            {
                _httpClientFactory = httpClientFactory;
                _daprClient = daprClient;
            }
            public string? TasksCreatedBy { get; set; }
    
            public IActionResult OnGet()
            {
                TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"];
    
                return (!String.IsNullOrEmpty(TasksCreatedBy)) ? Page() : RedirectToPage("../Index");
            }
    
            [BindProperty]
            public TaskAddModel? TaskAdd { get; set; }
    
            public async Task<IActionResult> OnPostAsync()
            {
                if (!ModelState.IsValid)
                {
                    return Page();
                }
    
                if (TaskAdd != null)
                {
                    var createdBy = Request.Cookies["TasksCreatedByCookie"];
    
                    if (!string.IsNullOrEmpty(createdBy))
                    {
                        TaskAdd.TaskCreatedBy = createdBy;
    
                        // Dapr SideCar Invocation
                        await _daprClient.InvokeMethodAsync(HttpMethod.Post, "tasksmanager-backend-api", $"api/tasks", TaskAdd);
                    }
                }
    
                return RedirectToPage("./Index");
            }
        }
    }
    
    using Dapr.Client;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    using TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks.Models;
    
    namespace TasksTracker.WebPortal.Frontend.Ui.Pages.Tasks
    {
        public class EditModel : PageModel
        {
            private readonly IHttpClientFactory _httpClientFactory;
            private readonly DaprClient _daprClient;
    
            [BindProperty]
            public TaskUpdateModel? TaskUpdate { get; set; }
            public string? TasksCreatedBy { get; set; }
    
            public EditModel(IHttpClientFactory httpClientFactory, DaprClient daprClient)
            {
                _httpClientFactory = httpClientFactory;
                _daprClient = daprClient;
            }
    
            public async Task<IActionResult> OnGetAsync(Guid? id)
            {
                TasksCreatedBy = Request.Cookies["TasksCreatedByCookie"];
    
                if (String.IsNullOrEmpty(TasksCreatedBy)) {
                    return RedirectToPage("../Index");
                }
    
                if (id == null)
                {
                    return NotFound();
                }
    
                // Dapr SideCar Invocation
                var Task = await _daprClient.InvokeMethodAsync<TaskModel>(HttpMethod.Get, "tasksmanager-backend-api", $"api/tasks/{id}");
    
                if (Task == null)
                {
                    return NotFound();
                }
    
                TaskUpdate = new TaskUpdateModel()
                {
                    TaskId = Task.TaskId,
                    TaskName = Task.TaskName,
                    TaskAssignedTo = Task.TaskAssignedTo,
                    TaskDueDate = Task.TaskDueDate,
                };
    
                return Page();
            }
    
    
            public async Task<IActionResult> OnPostAsync()
            {
                if (!ModelState.IsValid)
                {
                    return Page();
                }
    
                if (TaskUpdate != null)
                {
                    // Dapr SideCar Invocation
                    await _daprClient.InvokeMethodAsync<TaskUpdateModel>(HttpMethod.Put, "tasksmanager-backend-api", $"api/tasks/{TaskUpdate.TaskId}", TaskUpdate);
                }
    
                return RedirectToPage("./Index");
            }
        }
    }
    
Tip

Notice how we are not using the HttpClientFactory anymore and how we were able from the Frontend Dapr Sidecar to invoke backend API Sidecar using the method InvokeMethodAsync which accepts the Dapr remote App ID for the Backend API tasksmanager-backend-api and it will be able to discover the URL and invoke the method based on the specified input params.

In addition to this, notice how in POST and PUT operations, the third argument is a TaskAdd or TaskUpdate Model, those objects will be serialized internally (using System.Text.JsonSerializer) and sent as the request payload. The .NET SDK takes care of the call to the Sidecar. It also deserializes the response in case of the GET operations to a List<TaskModel> object.

Looking at the first option of invoking the remote service "Invoke HTTP services using HttpClient", you can see that we can create an HttpClient by invoking DaprClient.CreateInvokeHttpClient and specify the remote service app id, custom port if needed and then use the HTTP methods such as GetFromJsonAsync, this is a good approach as well at it gives you full support of advanced scenarios, such as custom headers and full control over request and response messages.

In both options, the final request will be rewritten by the Dapr .NET SDK before it gets executed. In our case and for the GET operation it will be written to this request: http://127.0.0.1:3500/v1/invoke/tasksmanager-backend-api/method/api/tasks?createdBy=tjoudeh@bitoftech.net

2.4 Run ACA Web - Frontend and ACA API - Backend Locally Using Dapr

We are ready now to verify the changes on the Frontend Web App and test locally. Therefore, we need to run the Frontend Web App along with the Backend Web API to ensure the Dapr Sidecar containers are working as expected.

  • Open another terminal inside VS Code, so that we can run the two commands shown below (ensure that you are on the right project directory when running each command).

  • In the second terminal, run .\Variables.ps1 to apply all session variables.

  • Obtain the local frontend UI URL to test shortly once the frontend UI and backend API are running in the next step.

    $FRONTEND_UI_BASE_URL_LOCAL
    
  • In each of the two terminals previously opened, run the frontend UI and backend API respectively.

    1
    2
    3
    4
    5
    6
    7
    8
    cd ~\TasksTracker.ContainerApps\TasksTracker.WebPortal.Frontend.Ui
    
    dapr run `
    --app-id tasksmanager-frontend-webapp `
    --app-port $UI_APP_PORT `
    --dapr-http-port 3501 `
    --app-ssl `
    -- dotnet run --launch-profile https
    
    1
    2
    3
    4
    5
    6
    7
    8
    cd ~\TasksTracker.ContainerApps\TasksTracker.TasksManager.Backend.Api
    
    dapr run `
    --app-id tasksmanager-backend-api `
    --app-port $API_APP_PORT `
    --dapr-http-port 3500 `
    --app-ssl `
    -- dotnet run --launch-profile https
    

Notice how we assigned the Dapr App Id “tasksmanager-frontend-webapp” to the Frontend WebApp.

Note

If you need to run both microservices together, you need to keep calling dapr run manually each time in the terminal. And when you have multiple microservices talking to each other you need to run at the same time to debug the solution. This can be a convoluted process. You can refer to the debug and launch Dapr applications in VSCode to see how to configure VScode for running and debugging Dapr applications.

2.5 Test ACA Web - Frontend and ACA API - Backend Locally Using Dapr

Success

Now both Applications are running using Dapr sidecar. Note how ports 3500 and 3501 are used when starting the container apps. These ports instruct the container runtime to communicate with the Dapr sidecar, whereas the https ports from the appsettings files are the ports you use to launch the application locally. Open the local frontend UI URL (use $FRONTEND_UI_BASE_URL_LOCAL from step 2.4), ignore the certificate warning locally, then provide an email to load the tasks for the user (e.g. tjoudeh@bitoftech.net). If the application is working as expected you should see tasks list associated with the email you provided.

  • Close the sessions and navigate to the root.

  • From the root, persist a list of all current variables.

    git add .\Variables.ps1
    git commit -m "Update Variables.ps1"
    
  • Navigate to the root and persist the module to Git.

    git add .
    git commit -m "Add Module 3"
    

Review

In this module, we have accomplished two objective:

  1. Introduced the Distributed Application Runtime (Dapr).
  2. Decoupled ACA Web - Frontend from ACA API - Backend locally via Dapr.

In the next module, we will integrate the Dapr state store building block by saving tasks to Azure Cosmos DB. We will also deploy the updated applications to Azure Container Apps.