Prerequisition
- Dapr: cli 1.0, Runtime 1.0
- .NET 5 SDK: 5.0.103
- Dapr client: Dapr.AspNetCore 1.0.0
- Tye: 0.7.0-alpha.21070.7+eb3b50699b7a5f2d0997a5cc8c5185d056dde8ec
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);
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");
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));
}
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);
}
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>();
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
});
}
}
The main codes to notice as
// check country is exists and valid
var (countryDto, isError, _) = await _countryApi.GetCountryByIdAsync(request.Model.CountryId);
Now let use tye
to init the core components:
$ tye run
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"
}
}
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
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
...
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)