DEV Community

Thang Chung
Thang Chung

Posted on • Updated on

How to make Dapr client works well with Refit and RestEase in 5 minutes

Prerequisition

If you want to go directly to the source code, then it is at https://github.com/thangchung/practical-clean-ddd ⭐⭐⭐⭐⭐

Begin story

With the Dapr client in code, we need to follow its patterns and the way to call another Daprized App to its Rest API. Normally, we use service invocation to the Dapr server app. The code as below

var client = DaprClient.CreateInvokeHttpClient(appId: "routing");
var deposit = new Transaction  { Id = "17", Amount = 99m };
var response = await client.PostAsJsonAsync("/deposit", deposit, cancellationToken);
var account = await response.Content.ReadFromJsonAsync<Account>(cancellationToken: cancellationToken);
Enter fullscreen mode Exit fullscreen mode

And this way of calling is really easy to make Leaky Abstraction
But what we think in my mind is something like this

public interface IRoutingApi
{
    [Post("/deposits")]
    Task<Account> DepositTransaction([Body] Transaction body);
}

var account = RestService.For<IRoutingApi>("http://routing");
Enter fullscreen mode Exit fullscreen mode

We can use Refit or RestEase to make it better to read and use just like above.

It must be easy and in a Rest-way to call the Rest API, right? Make Rest back to its natural power.

With Dapr.AspnetCore 1.0 we can luckily make Dapr client work naturally with Refit or RestEase.

Ryan Nowak had worked on the task to make it work together really well 👍. You can find the version of Dapr client with Refit at https://github.com/rynowak/dapr-httpclient-extravaganza/blob/main/samples/BankClient/RefitExample.cs, and the official implementation of the adaptor for Dapr client can be found at https://github.com/dapr/dotnet-sdk/blob/master/src/Dapr.Client/InvocationHandler.cs

Dapr loves RestEase as well ❤️❤️❤️

In this article, we focus on how to make Dapr client work with RestEase. Let imagine that we have a service with the name customer service which lets end-user register their information. And we should have the Rest API like POST http://localhost:5000/customer-api/customers, then in this API we call to another service, let says setting service to check whether the country of customer is valid or not? We have the Rest API for setting service just like GET http://settingapp:5005/api/countries/{id}, the code for this API as below

[HttpGet("/api/countries/{id:guid}")]
public override async Task<ActionResult<CountryDto>> HandleAsync(Guid id,
    CancellationToken cancellationToken = new())
{
    var request = new Query {Id = id};

    return Ok(await Mediator.Send(request, cancellationToken));
}
Enter fullscreen mode Exit fullscreen mode

Now we set up the code to call this API in customer service as below

Define the API contract:

// ICountryApi.cs
public interface ICountryApi
{
    [Get("api/countries/{countryId}")]
    Task<ResultModel<CountryDto>> GetCountryByIdAsync([Path] Guid countryId);
}
Enter fullscreen mode Exit fullscreen mode

Wire up some code in the Startup.cs

// Startup.cs
var settingAppUri = IsRunOnTye
    ? $"http://{AppConsts.SettingAppName}:5005"
    : "http://localhost:5005"; 

services.AddScoped<InvocationHandler>();
services.AddRestEaseClient<ICountryApi>(settingAppUri, client =>
{
    client.RequestPathParamSerializer = new StringEnumRequestPathParamSerializer();
}).AddHttpMessageHandler<InvocationHandler>();
Enter fullscreen mode Exit fullscreen mode

And the code in the handler:

// CreateCustomer.cs
internal class Handler : IRequestHandler<Command, ResultModel<CustomerDto>>
{
    private readonly IRepository<Customer> _customerRepository;
    private readonly ICountryApi _countryApi;

    public Handler(IRepository<Customer> customerRepository,
        ICountryApi countryApi)
    {
        _customerRepository = customerRepository ?? throw new ArgumentNullException(nameof(customerRepository));
        _countryApi = countryApi ?? throw new ArgumentNullException(nameof(countryApi));
    }

    public async Task<ResultModel<CustomerDto>> Handle(Command request,
        CancellationToken cancellationToken)
    {
        var alreadyRegisteredSpec = new CustomerAlreadyRegisteredSpec(request.Model.Email);

        var existingCustomer = await _customerRepository.FindOneAsync(alreadyRegisteredSpec);

        if (existingCustomer != null)
            throw new Exception("Customer with this email already exists");

        // check country is exists and valid
        var (countryDto, isError, _) = await _countryApi.GetCountryByIdAsync(request.Model.CountryId);
        if (isError || countryDto.Id.Equals(Guid.Empty))
        {
            throw new Exception("Country Id is not valid.");
        }

        var customer = Customer.Create(request.Model.FirstName, request.Model.LastName, request.Model.Email, request.Model.CountryId);

        var created = await _customerRepository.AddAsync(customer);

        return new ResultModel<CustomerDto>(new CustomerDto
        {
            Id = created.Id,
            FirstName = created.FirstName,
            LastName = created.LastName,
            Email = created.Email,
            CountryId = created.CountryId
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The main codes to notice as

// check country is exists and valid
var (countryDto, isError, _) = await _countryApi.GetCountryByIdAsync(request.Model.CountryId);
Enter fullscreen mode Exit fullscreen mode

Now let use tye to init the core components:

$ tye run
Enter fullscreen mode Exit fullscreen mode

Go to http://localhost:8000, and make sure all the apps running well.

Finally, you make a call to POST http://localhost:5000/customer-api/customers with the body as

{
  "model": {
    "firstName": "firstName 1",
    "lastName": "lastName 1",
    "email": "email1@nomail.com",
    "countryId": "18a4a8ae-3338-484a-a4ed-6e64d13d84dc"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you can see the logs as below:

[customerapp_5ed4a92a-f]: info: System.Net.Http.HttpClient.CoolStore.AppContracts.RestApi.ICountryApi.LogicalHandler[100]
[customerapp_5ed4a92a-f]: Start processing HTTP request GET http://settingapp:5005/api/countries/18a4a8ae-3338-484a-a4ed-6e64d13d84dc
[customerapp_5ed4a92a-f]: info: System.Net.Http.HttpClient.CoolStore.AppContracts.RestApi.ICountryApi.ClientHandler[100]
[customerapp_5ed4a92a-f]: Sending HTTP request GET http://127.0.0.1:50910/v1.0/invoke/settingapp/method/api/countries/18a4a8ae-3338-484a-a4ed-6e64d13d84dc
[customerapp_5ed4a92a-f]: info: System.Net.Http.HttpClient.CoolStore.AppContracts.RestApi.ICountryApi.ClientHandler[101]
[customerapp_5ed4a92a-f]: Received HTTP response headers after 695.0478ms - 200
[customerapp_5ed4a92a-f]: info: System.Net.Http.HttpClient.CoolStore.AppContracts.RestApi.ICountryApi.LogicalHandler[101]
[customerapp_5ed4a92a-f]: End processing HTTP request after 710.2258ms - 200
Enter fullscreen mode Exit fullscreen mode

Have you noticed some lines of logs above at

...
Start processing HTTP request GET http://settingapp:5005/api/countries/18a4a8ae-3338-484a-a4ed-6e64d13d84dc
...
Sending HTTP request GET http://127.0.0.1:50910/v1.0/invoke/settingapp/method/api/countries/18a4a8ae-3338-484a-a4ed-6e64d13d84dc
...
Enter fullscreen mode Exit fullscreen mode

Here ya go! Dapr client helps us to handle the work which is mainly on RestEase way.

Easy peasy, right? Give the star ⭐ for the repository below if you feel like this article helps you out. Thank you! ❤️❤️❤️

Source code of this article can be found at https://github.com/thangchung/practical-clean-ddd ⭐⭐⭐⭐⭐

Top comments (0)