Dapr-6 Dapr service invocation building blocks

Chapter 6 Dapr Service Invocation Building Block

https://docs.microsoft.com/en-us/dotnet/architecture/dapr-for-net-developers/service-invocation

Throughout distributed systems, services often need to communicate with other services to complete business operations. The Dapr service invocation building block can help simplify communication between services.

6.1 What kind of problem is solved

Calling between services in a distributed system may seem simple, but it involves various challenges. For example:

  • How to locate other services
  • How to safely call other services, specific service addresses
  • How to handle retries when brief transient errors occur

Finally, because distributed applications are aggregated from a variety of disparate services, insight into the call graph between services is key to diagnosing product issues.

The service invocation building block handles such challenges by using the Dapr sidecar as a reverse proxy.

6.2 How it works

Let’s start with an example. Consider there are two services, “Service A” and “Service B”.

Service A needs to call Service B’s catalog/items API. Although service A can depend on service B and call it directly, service A calls the Dapr sidecar’s service call API. Figure 6-1 shows the implementation of the operation.

Figure 6-1 How Dapr service invocation works

Note the steps called in the picture above:

  1. Service A calls the catalog/items API of service B by calling the service call API of service A’s sidecar.

    Notice
    The sidecar uses a pluggable name resolution component to resolve the address of service B. In self-hosted mode, Dapr uses mDNS for queries. When running in Kubernetes mode, the Kubernetes DNS service determines this address.

  2. Service A’s sidecar forwards the request to Service B’s sidecar
  3. Service B’s sidecar makes the actual call to Service B’s catalog/items API
  4. Service B performs request processing and returns the response to its sidecar
  5. Service B’s sidecar forwards the response back to Service A’s sidecar
  6. Service A’s sidecar returns the response to Service A

Since calls need to go through a sidecar, Dapr can inject some useful crosscutting behavior:

  • Automatic retries for failed calls
  • Use mutual (mTLS) authentication for calls between services, including automatic certificate rollover
  • Use access control policies to control what clients can do
  • For all calls between services, capture traces and metrics information to provide insights and diagnostics

Any application can invoke Dapr sidecar using Dapr’s built-in invoke API. The API can use both HTTP and gRPC methods. Use the following URL to call the HTTP API:

http://localhost:<dapr-port>/v1.0/invoke/<application-id>/method/<method-name>
  • The port number Dapr listens on

  • The application ID of the service call

  • The method name to call the remote service

In the following example, the example’s curl call accesses Service B’s catalog/items GET endpoint.

curl http://localhost:3500/v1.0/invoke/serviceb/method/catalog/items

Notice
The Dapr API supports the use of Dapr building blocks by any application that can support HTTP or gRPC. Furthermore, the service call building block can serve as a bridge between protocols. Services can communicate with each other using HTTP, gRPC, or both.

In the next section, you’ll learn how to use the .NET SDK to simplify service calls.

6.2 Use Dapr .NET SDK

The Dapr .NET SDK provides .NET developers an intuitive and language-specific way to interact with Dapr. The SDK provides developers with 3 ways to call remote services:

  1. Use HttpClient to call remote HTTP services
  2. Use DaprClient to call remote HTTP services
  3. Use DaprClient to call remote gRPC services

6.2.1 Use HttpClient to call remote HTTP service

The recommended way to call HTTP endpoints is to use the enhanced version of HttpClient provided by Dapr. The following example submits an order by calling the orderservice‘s submit() method:

var httpClient = DaprClient.CreateInvokeHttpClient();
await httpClient.PostAsJsonAsync("http://orderservice/submit", order);

In this example, the DaprClient.CreateInvokeHttpClient() method returns a HttpClient instance that is used to perform Dapr service calls. The returned HttpClient uses a specially crafted Dapr message handler to rewrite the URL of the outgoing call request. The name of the host is interpreted as the application ID of the called service. The rewritten request is actually called as follows:

http://127.0.0.1:3500/v1/invoke/orderservice/method/submit

This example uses the default value for the Dapr HTTP endpoint, which is of the form http://127.0.0.1:/. The actual value of dapr-http-port is obtained through the environment variable DAPR_HTTP_PORT. If not set, the default port number will be 3500.

Additionally, you can configure a custom endpoint in a call to the DaprClient.CreateInvokeHttpClient() method:

var httpClient = DaprClient.CreateInvokeHttpClient(
        daprEndpoint = "localhost:4000");

You can also set the base address by specifying the application ID. This allows you to use relative addresses when calling:

var httpClient = DaprClient.CreateInvokeHttpClient("orderservice");
await httpClient.PostAsJsonAsync("/submit");

The HttpClient object tends to be long-lived. A single HttpClient instance can be reused throughout the application life cycle. The following scenario demonstrates how OrderServiceClient can reuse Dapr’s HttpClient instance.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IOrderServiceClient, OrderServiceClient>(
    _ => new OrderServiceClient(DaprClient.CreateInvokeHttpClient("orderservice")));

In the above code snippet, OrderServiceClient is registered as a singleton with the ASP.NET Core dependency injection system. In the implemented factory, a new HttpClient object instance is created by calling the DaprClient.CreateInvokeHttpClient() method. By registering OrderServiceClient as a singleton, it will be reused throughout the application lifecycle.

OrderServiceClient itself has no Dapr-specific code. Although Dapr service calls are used under the hood, you can think of Httpclient equivalent to other HttpClients.

public class OrderServiceClient : IOrderServiceClient
{
    private readonly HttpClient _httpClient;

    public OrderServiceClient(HttpClient httpClient)
    {
        _httpClient = httpClient  throw new ArgumentNullException(nameof(httpClient));
    }

    public async Task SubmitOrder(Order order)
    {
        var response = await _httpClient.PostAsJsonAsync("submit", order);
        response.EnsureSuccessStatusCode();
    }
}

There are several advantages to using the HttpClient class for Dapr service calls:

  • HttpClient is a well-known class that most developers have used in their code. Using HttpClient for Dapr service calls allows developers to reuse existing knowledge and skills.
  • HttpClient supports advanced scenarios such as custom request headers and full control over request and response messages
  • In .NET 5, HttpClient supports automatic serialization and deserialization using System.Text.Json
  • HttpClient is integrated into various existing frameworks and libraries, such as Refit, RestSharp and Polly

6.2.2 Use DaprClient to call HTTP service

Although HttpClient is the recommended way to invoke services using HTTP semantics, you can also use methods from the DaprClient.InvokeMethodAsync() family. The following example submits an order by calling the sumit() method of the orderservice service:

var daprClient = new DaprClientBuilder().Build();
try
{
    varconfirmation=
        await daprClient.InvokeMethodAsync<Order, OrderConfirmation>(
            "orderservice", "submit", order);
}
catch (InvocationException ex)
{
    // Handle error
}

The third parameter, order, is internally serialized (using System.Text.JsonSerializer) and then sent as the content of the request. The .NET SDK is responsible for calling the sidecar. It also deserializes the response content into an OrderConfirmation object. Because no HTTP method is specified, the request will be performed using HTTP POST.

The following example shows how to make an HTTP GET request by specifying HttpMethod:

var catalogItems = await daprClient.InvokeMethodAsync<IEnumerable<CatalogItem>>(HttpMethod.Get, "catalogservice", "items");

For some scenarios, you may need more control over request messages. For example, when you need specific request headers, or you want to use a custom serializer to process the request content. The DaprClient.CreateInvokeMethodRequest() method creates a HttpRequestMessage object. The following example code shows how to add an HTTP authentication header to the request message:

var request = daprClient.CreateInvokeMethodRequest("orderservice", "submit", order);
request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);

The current HttpRequestMessage has the following properties set:

  • Url = http://127.0.0.1:3500/v1.0/invoke/orderservice/method/submit
  • HttpMethod = POST
  • Content = JsonContent object contains JSON serialized order
  • Headers.Authorization = “bearer

Once you set the settings on the Request object, use DaprClient.InvokeMethodAsync() to send it:

var orderConfirmation = await daprClient.InvokeMethodAsync<OrderConfirmation>(request);

If the request is successfully processed, DaprClient.InvokeMethodAsync deserializes the response contents into an OrderConfirmation object instance. Alternatively, you can use DaprClient.InvokeMethodWithResponseAsync to gain full control of the underlying HttpResponseMessage:

var response = await daprClient.InvokeMethodWithResponseAsync(request);
response.EnsureSuccessStatusCode();

var orderConfirmation = response.Content.ReadFromJsonAsync<OrderConfirmation>();

Notice
For service calls using HTTP, consider using the Dapr HttpClient method described in the previous section. Using HttpClient can provide additional advantages, such as integration with existing frameworks and libraries.

6.2.3 Use DaprClient to call gRPC service

DaprClient provides a set of InvokeMethodGrpcAsync() methods for invoking gRPC endpoints. The main difference from the HTTP approach is the use of the Protobuf protocol serializer instead of JSON. The following example calls the submitOrder method of orderservice via gRPC.

var daprClient = new DaprClientBuilder().Build();
try
{
    var confirmation = await daprClient.InvokeMethodGrpcAsync<Order, OrderConfirmation>("orderservice", "submitOrder", order);
}
catch (InvocationException ex)
{
    // Handle error
}

In the above example, DaprClient uses Protobuf to serialize the given order object and uses the result in the gRPC request body. Likewise, the response content is deserialized using Protobuf and returned to the caller. Protobuf specifically provides better performance than the JSON approach for HTTP service invocation.

6.3 Name Resolution Component

At the time of writing, Dapr provides support for the following name resolution components:

  • mDNS (used by default when running in self-hosted mode)
  • Kubernetes Name Resolution (used by default when running in Kubernetes mode)
  • HashiCorp Consul

6.3.1 Configuration

If using a non-default name resolution component, add the nameResolution specification to your application’s Dapr configuration file. The following example Dapr configuration file supports name resolution using HashiCorp Consul:

apiVersion: dapr.io/v1alpha1
Kind: Configuration
metadata:
  name: dapr-config
spec:
  nameResolution:
    component: "consul"
    configuration:
      selfRegister: true

6.4 Sample Application: Dapr Traffic Management

In the Dapr traffic management sample application, the Finecollection fines service uses the Dapr service call building block to obtain vehicle and owner information through the VehicleRegistration vehicle registration service. Figure 6-2 shows the conceptual architecture of the Dapr Traffic Management sample application. The Dapr service call building block is used in the portion of the flowchart labeled number 1.

Figure 6-2 Conceptual architecture of Dapr traffic management example application

Information is obtained through the ASP.NET CollectionController in the FineCollection fine service. The CollectFine method expects an incoming SpeedingViolation parameter. It calls the Dapr service call building block to call the VehicleRegistration service. The code snippet looks like this:

[Topic("pubsub", "speedingviolations")]
[Route("collectfine")]
[HttpPost]
public async Task<ActionResult> CollectFine(SpeedingViolation speedingViolation, [FromServices] DaprClient daprClient)
{
   // ...

   // get owner info (Dapr service invocation)
   var vehicleInfo = _vehicleRegistrationService.GetVehicleInfo(speedingViolation.VehicleId).Result;

   // ...
}

The code uses the VehicleRegistrationService proxy type to call the VehicleRegistration service. ASP.NET Core uses constructor injection to inject an instance of the service proxy:

public CollectionController(
    ILogger<CollectionController> logger,
    IFineCalculator fineCalculator,
    VehicleRegistrationService vehicleRegistrationService,
    DaprClient daprClient)
{
    // ...
}

The VehicleRegistrationService class contains a single method GetVehicleInfo(). It uses ASP.NET Core’s HttpClient to call the VehicleRegistration service:

public class VehicleRegistrationService
{
    private HttpClient _httpClient;
    public VehicleRegistrationService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<VehicleInfo> GetVehicleInfo(string licenseNumber)
    {
        return await _httpClient.GetFromJsonAsync<VehicleInfo>(
            $"vehicleinfo/{licenseNumber}");
    }
}

The code does not directly depend on any Dapr classes. Instead, it relies on the integration in ASP.NET Core from the Using HttpClient to Call HTTP Services section earlier in this module. The following code comes from the ConfigureService() method in the Startup class, which registers the VehicleRegistrationService agent:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<VehicleRegistrationService>(_ =>
    new VehicleRegistrationService(DaprClient.CreateInvokeHttpClient(
        "vehicleregistrationservice", $"http://localhost:{daprHttpPort}"
    )));

The DaprClient.CreateInvokeHttpClient() method creates a HttpClient instance and uses the service call building block under the hood to call the VehicleRegistration service. It requires the app-id of Dapr’s target service and the URL of Dapr’s sidecar. On startup, the daprHttpPort parameter contains the port number used to communicate with the Dapr sidecar.

Using Dapr service calls in the traffic management sample application provides the following advantages:

  1. Decouple the address of the target service
  2. Added flexibility to auto-retry feature
  3. Ability to reuse existing HttpClient based on proxy (available through ASP.NET Core integration)

6.5 Summary

In this chapter, you learned about the service invocation building blocks. Saw how to make direct HTTP calls to the Dapr sidecar approach, and the Dapr .NET SDK approach.

Dapr .NET SDK provides multiple ways to call remote methods. HttpClient is valuable for developers who want to reuse existing skills, and it is also compatible with a variety of existing frameworks and libraries. DaprClient provides Dapr service calls directly using HTTP or gRPC semantics.