In the previous post, I covered how to deploy a gRPC service to Azure Container App, and how to create a simple minimal Web API deployed to Azure Container Apps which acts as a gRPC client consuming the gRPC Service, In this post, I’ll cover the 2 reaming scenarios which I’ll enable Dapr on the Client and Service apps and see how we can call the gRPC service using GrpcClient and the DaprClient SDK.
Invoking Dapr Services in Azure Container Apps using gRPC
When Dapr is enabled, we can utilize Dapr service invocation API that acts similar to a reverse proxy with built-in service discovery, while leveraging built-in distributed tracing, transient error handling, and retry of transient failures, this will be an added value compared to calling services directly using GrpcClient without Dapr. Looking at the diagram below, the two scenarios I’ll cover will be using Dapr Sidecar of each service to call each other over gRPC, the gRPC client API will be exposed “externally” over HTTP so I can test using a traditional REST client.
The source code for this tutorial is available on GitHub.
Scenario 2: Invoke gRPC services via Dapr Sidecar using GrpcClient (Dapr enabled)
In this scenario, we’ll see that we can enable Dapr on both applications and it will allow us to keep using our own proto services defined in the previous post (expense.proto) without any change to the gRPC service. This means that we can use service invocation to call our existing gRPC services without having to include any Dapr client SDKs or include custom gRPC services.
Update gRPC Client API
Step 1: Update ‘GrpcClient’ Address Configuration
In order for the gRPC client API to invoke the gRPC service, we need to update the typed gRPC client configuration and use the injected “DAPR_GRPC_PORT” environment variable Dapr is enabled on the client API, the value of this environment variable is the gRPC port that the Dapr sidecar is listening on. Our gRPC client API should use this variable to connect to the Dapr sidecar instead of hardcoding the port value, to do this change, open “Program.cs” and add the highlighted lines below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
builder.Services.AddGrpcClient<ExpenseSvc.ExpenseSvcClient>(o => { var islocalhost = builder.Configuration.GetValue("grpc:localhost", false); var serverAddress = ""; if (islocalhost) { var port = "7029"; var scheme = "https"; var daprGRPCPort = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); if (!string.IsNullOrEmpty(daprGRPCPort)) { scheme = "http"; port = daprGRPCPort; } serverAddress = string.Format(builder.Configuration.GetValue<string>("grpc:server"), scheme, port); } else { serverAddress = builder.Configuration.GetValue<string>("grpc:server"); } o.Address = new Uri(serverAddress); }); |
Notice that when Dapr is enabled, the value of the Environment Variable “DAPR_GRPC_PORT” will not be empty and it will contain a port number, so we are going to use this port number and stop calling the gRPC server address directly, we are offloading the gRPC server service discovery to the gRPC Client Dapr Sidecar as I’ll show you in the next step.
Step 2: Inject Metadata headers upon invoking gRPC methods
Now I need to configure service discovery of the gRPC server by providing the Dapr Server App-Id when the gRPC client invokes a method on the server, to do this, we need to inject a “Metadata” header similar to the code below, so add the method below to your “Program.cs” file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
Metadata? BuildMetadataHeader() { //The gRPC port that the Dapr sidecar is listening on var daprGRPCPort = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); Metadata? metadata = null; if (!string.IsNullOrEmpty(daprGRPCPort)) { metadata = new Metadata(); var serverDaprAppId = "expenses-grpc-server"; metadata.Add("dapr-app-id", serverDaprAppId); app?.Logger.LogInformation("Calling gRPC server app id '{server}' using dapr sidecar on gRPC port: {daprGRPCPort}", serverDaprAppId, daprGRPCPort); } return metadata; } |
Notice how we are adding an entry to the “Metadata” dictionary with a key named “dapr-app-id” and a value of “expenses-grpc-server” which is the name of the gRPC service Dapr App Id, which we are going to set in the following steps.
Next, we need to call the method BuildMetadataHeader() upon calling each gRPC method, I will show below how to do it when calling the method “GetExpenseByIdAsync” and the other methods will follow the same pattern, check the highlighted line below:
1 2 3 4 5 6 7 8 9 10 11 |
app.MapGet("/api/expenses/{id}", async (ExpenseSvc.ExpenseSvcClient grpcClient, int id) => { GetExpenseByIdResponse? response; var request = new GetExpenseByIdRequest { Id = id }; app?.Logger.LogInformation("Calling grpc server (GetExpenseByIdRequest) for id: {id}", id); response = await grpcClient.GetExpenseByIdAsync(request, BuildMetadataHeader()); return Results.Ok(response.Expense); }).WithName("GetExpenseById"); |
With this change on gRPC client, we are ready to test the gRPC service and client locally using Dapr CLI.
Enable Dapr and test the gRPC server and client locally
Now I’ll run the gRPC server while Dapr is enabled, to do so, navigate to the root folder of the project “Expenses.Grpc.Server” and run the command below. If you don’t have Dapr CLI installed locally on your machine you can check my previous post for more details:
1 |
dapr run --app-id expenses-grpc-server --app-protocol grpc --app-port 7029 --app-ssl -- dotnet run |
When using dapr run command we are running a dapr process as a sidecar next to the gRPC Server, the properties we have configured as the following:
- app-id: The unique identifier of the application. Used for service discovery, the value of this parameter is: “expenses-grpc-server” and it should match what we used in the “BuildMetadataHeader()” method.
- app-port: This parameter tells Dapr which port your application is listening on, you can get the app port from “launchSettings.json” file in the gRPC service project, I’m using the https port here.
- app-protocol: This is a gRPC service, setting this to “grpc”.
- app-ssl: Sets the URI scheme of the app to https and attempts an SSL connection.
Next, I will run the gRPC client API while Dapr is enabled, to do so, navigate to the root folder of the project “Expenses.Grpc.Api” and run the command below:
1 |
dapr run --app-id expenses-grpc-api --app-protocol http --app-port 5252 --dapr-http-port 3501 -- dotnet run |
The properties we have configured for the gRPC client as the following:
- app-id: Using the value “expenses-grpc-api”.
- app-protocol: gRPC client API is a standard Web API, so the protocol is HTTP.
- dapr-http-port: The HTTP port for Dapr to listen on, setting it to 3501.
To test locally, using any Rest client, send HTTP GET request to the endpoint http://localhost:3501/v1.0/invoke/expenses-grpc-api/method/api/expenses/2 notice how I’m calling the Dapr Sidecar “Invoke” API of the gRPC client API with app-id “expenses-grpc-api” on port 3501, internally the gRPC client API will invoke the gRPC Server Sidecar over gRPC protocol and call the method “GetExpenseById” in the gRPC service.
Enable Dapr on the gRPC Server and Client then deploy updated to Azure Container Apps
Now we need to ship the changes done on the gRPC client API to Azure Container Apps and enable Dapr on the gRPC service and client too.
Step 1: Enable Dapr on the gRPC Server Azure Container App
We didn’t do any code changes on the server, so there is no need to build and push a new image, nor to deploy a new revision of the Azure Container App, all we need to do is to enable Dapr on the gRPC server Azure Container App, to do so, run the below CLI command:
1 2 3 4 5 6 7 8 |
az containerapp dapr enable ` --name $BACKEND_SVC_NAME ` --resource-group $RESOURCE_GROUP ` --dal ` --dapr-app-id $BACKEND_SVC_NAME ` --dapr-app-port 80 ` --dapr-app-protocol grpc ` --dapr-log-level info |
Notice how I’m setting the property “dapr-app-protocol” to “grpc”, this will tell Dapr Sidecar that this service is using gRPC protocol.
Step 2: Update gRPC client API and create a new Azure Container App Revision
To reflect changes done on the gRPC client API, we need to create and push a new image to the container registry used, once this is done, we need to create a new revision using the command below:
1 2 3 4 5 |
az containerapp update ` --name $BACKEND_API_NAME ` --resource-group $RESOURCE_GROUP ` --revision-suffix v20221102-1 ` --set-env-vars "grpc__server={0}://localhost:{1}" "grpc__localhost=true" |
Notice that I’m setting the environment variables with new values so that when the gRPC client API constructs the gRPC server address, it will use the assigned “Dapr Grpc Port” as described in the previous steps.
Step 3: Enable Dapr on the gRPC Client API Azure Container App
Similar to what I’ve done on the gRPC Server, we need to enable Dapr on the gRPC client API too, to do so, run the command below:
1 2 3 4 5 6 7 8 |
az containerapp dapr enable ` --name $BACKEND_API_NAME ` --resource-group $RESOURCE_GROUP ` --dal ` --dapr-app-id $BACKEND_API_NAME ` --dapr-app-port 80 ` --dapr-app-protocol grpc ` --dapr-log-level info |
We are setting the “dapr-app-protocol” to “grpc” for communication between the service and its Sidecar but remember from the previous post that Azure Container App hosting the gRPC Client API is using “transport” of type “http” that’s why we can invoke it using standard HTTP request as we will see in the next step.
With this in place, we can do our final testing via PostMan or any other REST client, take note of your gRPC client API Container App FQDN and invoke the POST operation to create a new expense, PostMan result should look like the below:
Note on distributed tracing of Dapr Services:
If you configured Application Insights when creating the Azure Container Apps Environment by setting the parameter “–dapr-instrumentation-key” to App Insights instrumentation key; you will be able to see the distributed tracing between the gRPC client API and the gRPC server on App Insights Application Map, this won’t be available if you are using the first approach (Scenario 1 in the previous post). The application map should be similar to the image below:
Scenario 3: Invoke gRPC services via Dapr Sidecar using DaprClient SDK (Dapr enabled)
Dapr offers a .NET SDK which enables us to invoke the gRPC services using a different approach other than the standard “GrpcClient” used in the previous 2 scenarios. If we are going to use this approach and utilize the methods available on Dapr .NET SDK such as method “InvokeMethodGrpcAsync” we need to build the gRPC service in a different way by implementing Dapr appcallback service, yet there is no change on proto files.
Create a new gRPC service inheriting from ‘AppCallback.AppCallbackBase’
Step 1: Create a new gRPC service
The first thing to do is to install the NuGet package “Dapr.AspNetCore” on project “Expenses.Grpc.Server”, then add a new file named “ExpenseServiceAppCallBack.cs” under the folder “Services”, this new service will do the exact behavior done by the previous ExpensesService, I will paste the content of the file here and go over the important parts of it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
using Dapr.AppCallback.Autogen.Grpc.v1; using Dapr.Client.Autogen.Grpc.v1; using Google.Protobuf.WellKnownTypes; using Grpc.Core; namespace Expenses.Grpc.Server.Services { public class ExpenseServiceAppCallBack : AppCallback.AppCallbackBase { private readonly ILogger<ExpenseServiceAppCallBack> _logger; private readonly IExpensesRepo _expensesRepo; public ExpenseServiceAppCallBack(IExpensesRepo expensesRepo, ILogger<ExpenseServiceAppCallBack> logger) { _expensesRepo = expensesRepo; _logger = logger; } public override Task<InvokeResponse> OnInvoke(InvokeRequest request, ServerCallContext context) { var response = new InvokeResponse(); switch (request.Method) { case "GetExpenses": var getExpensesRequestInput = request.Data.Unpack<GetExpensesRequest>(); var getExpensesResponseOutput = new GetExpensesResponse(); _logger.LogInformation("Getting expenses for owner: {owner}", getExpensesRequestInput.Owner); var filteredResults = _expensesRepo.GetExpensesByOwner(getExpensesRequestInput.Owner); getExpensesResponseOutput.Expenses.AddRange(filteredResults); response.Data = Any.Pack(getExpensesResponseOutput); break; case "GetExpenseById": var getExpenseByIdRequestInput = request.Data.Unpack<GetExpenseByIdRequest>(); var getExpenseByIdResponseOutput = new GetExpenseByIdResponse(); _logger.LogInformation("Getting expense by id: {id}", getExpenseByIdRequestInput.Id); var expense = _expensesRepo.GetExpenseById(getExpenseByIdRequestInput.Id); getExpenseByIdResponseOutput.Expense = expense; response.Data = Any.Pack(getExpenseByIdResponseOutput); break; case "AddExpense": var addExpenseRequestInput = request.Data.Unpack<AddExpenseRequest>(); var addExpenseResponseOutput = new AddExpenseResponse(); _logger.LogInformation("Adding expense for provider {provider} for owner: {owner}", addExpenseRequestInput.Provider, addExpenseRequestInput.Owner); var expenseModel = new ExpenseModel() { Owner = addExpenseRequestInput.Owner, Amount = addExpenseRequestInput.Amount, Category = addExpenseRequestInput.Category, Provider = addExpenseRequestInput.Provider, Workflowstatus = addExpenseRequestInput.Workflowstatus, Description = addExpenseRequestInput.Description }; _expensesRepo.AddExpense(expenseModel); addExpenseResponseOutput.Expense = expenseModel; response.Data = Any.Pack(addExpenseResponseOutput); break; default: break; } return Task.FromResult(response); } } } |
What I’ve done here is the following:
- The service inherits from the abstract class “AppCallback.AppCallbackBase”, this is needed as it will be called by the Dapr runtime to invoke gRPC method.
- I’m overriding the method “OnInvoke” which is called when service invocation is happening, this method accepts an input parameter of type “InvokeRequest” which contains the following properties:
- A string property named “Method” holds the name of the method which is invoked by the caller. In our case, we are supporting three methods “GetExpenses”, “GetExpenseById”, and “AddExpense”. Those method names should be identical to the names defined in the “expense.proto” file definition.
- A “Data” property of type “Google.Protobuf.WellKnownTypes.Any”, this property holds a serialized protocol buffer message. I’m calling “Unpack” and specifying the expected input type request to serialize the data into a strongly typed object.
- Using the injected “IExpenseRepo” I’m calling the right operation to manipulate the In-memory Expenses list.
- Lastly, I’m generating a response output based on the invoked method and returning the response after “packing” the specified message into an “Any” message and assigning it to the “Data” property of an “InvokeResponse” object.
Step 2: Add “ExpenseServiceAppCallBack” gRPC service to the routes pipeline
Now we need to add the “ExpenseServiceAppCallBack” gRPC service to the routing pipeline so clients can invoke the operation “InvokeMethodGrpcAsync”, to do so, open the file “Program.cs” and paste the code below:
1 |
app.MapGrpcService<ExpenseServiceAppCallBack>(); |
Update gRPC Client API to use DaprClient SDK
Step 1: Install DaprClient SDK into gRPC Client API
Now we need to install the NuGet package “Dapr.AspNetCore” on project “Expenses.Grpc.Api”, after it gets installed, we need to register the DaprClient into service collection, so open Program.cs file and add builder.Services.AddDaprClient();
Step 2: Use DaprClient for methods invocations
Next, I’m injecting the “DaprClient” into each defined route endpoint, and I’m calling the method “InvokeMethodGrpcAsync”, so for example the updated code of the endpoint “/api/expenses/{id}” will look like the below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
app.MapGet("/api/expenses/{id}", async (ExpenseSvc.ExpenseSvcClient grpcClient, Dapr.Client.DaprClient daprClient, int id) => { GetExpenseByIdResponse? response; var request = new GetExpenseByIdRequest { Id = id }; if (builder.Configuration.GetValue("grpc:daprClientSDK", false)) { app?.Logger.LogInformation("DaprClientSDK::Calling grpc server (GetExpenseByIdRequest) for id: {id}", id); response = await daprClient.InvokeMethodGrpcAsync<GetExpenseByIdRequest, GetExpenseByIdResponse>("expenses-grpc-server", "GetExpenseById", request); } else { app?.Logger.LogInformation("Calling grpc server (GetExpenseByIdRequest) for id: {id}", id); response = await grpcClient.GetExpenseByIdAsync(request, BuildMetadataHeader()); } return Results.Ok(response.Expense); }).WithName("GetExpenseById"); |
Notice in the highlighted line above how I’m specifying the gRPC server Dapr “App-Id”, and invoking the method named “GetExpenseById” hosted in this server.
Note: We can completely remove the reference to the “GrpcClient” if we are going to use the “DaprClient” but I kept the code as is and introduced an Environment Variable named “grpc:daprClientSDK” and set it to “true” when using “DaprClient”. With this approach; the code on gRPC client API will remain working whether you decided to use “GrpcClient” or “DaprClient”.
Step 3: Test the gRPC client using DaprClient SDK locally
Now I’ll run both the gRPC server and gRPC Client API while Dapr is enabled locally to do so, run both commands below, and don’t forget to change the directory to the root folder of each project:
Run gRPC Server:
1 |
dapr run --app-id expenses-grpc-server --app-protocol grpc --app-port 7029 --app-ssl -- dotnet run |
Run gRPC Client API:
1 |
dapr run --app-id expenses-grpc-api --app-protocol http --app-port 5252 --dapr-http-port 3501 -- dotnet run |
To test locally, using any Rest client, send HTTP GET request to the endpoint http://localhost:3501/v1.0/invoke/expenses-grpc-api/method/api/expenses/2 notice how logs generated from the gRPC server are using the new “ExpenseServiceAppCallBack” gRPC service, logs should look similar to the below image:
Deploy changes to Azure Container Apps
Lastly, we need to push changes done on the gRPC server and gRPC Client API to Azure Container Apps and create 2 new revisions, to do so, create 2 new images for both applications and push them to your container registry, then update Azure Container Apps by running the two CLI commands:
Update gRPC Server Azure Container App
1 2 3 4 |
az containerapp update ` --name $BACKEND_SVC_NAME ` --resource-group $RESOURCE_GROUP ` --revision-suffix v20221103-1 |
Update gRPC Client Api Azure Container App, notice how I’m setting the environment variable “grpc:daprClientSDK” to “true” to instruct the client API to use DaprClient SDK, not the GrpcClient.
1 2 3 4 5 |
az containerapp update ` --name $BACKEND_API_NAME ` --resource-group $RESOURCE_GROUP ` --revision-suffix v20221103-1 ` --set-env-vars "grpc__daprClientSDK=true" |
With this in place, you can do your final testing using any Rest Client, if you open Application Insights and checked the Request details, you will notice that there are extra properties injected when using DaprClient SDK for service-to-service invocation over gRPC, check the image below
The source code for this tutorial is available on GitHub.
Conclusion
In these two posts, I’ve covered the various scenarios in which we can invoke service using synchronous communication using GrpcClient or DaprSlient SDK and host the services on Azure Container Apps.