When creating systems, developers are faced with common problems that they use in many of their applications. In this article, I will touch on one of these problems and show how I solve it.
Most modern systems are built on microservice architecture. This means that a company can have many services, each of which is horizontally scalable and most likely has several instances. Thus, a request from a client may pass through many parts of the application before receiving a response.
As a result, developers have a natural need to track the entire chain of actions occurring with the client’s request. To do this, a single Request Id comes to their aid.
Request id serves the following purposes:
Typically, the Request Id is generated by the ingress controller of your system and then passed into all requests through headers.
Let's imagine that we are creating one of the backend systems. Our task is to read the Request Id from the request header and send it in the request header to the next system; we also need to use it in the logs and send it to the tracer, in general, it should be easily accessible from any class (May the DI be with you).
Let's start implementation. To start, we need a class in which we will store our Request Id.
public class SessionData
{
private string _requestId = Guid.NewGuid().ToString();
public string RequestId
{
get => _requestId;
set
{
if (!string.IsNullOrEmpty(value))
_requestId = value;
}
}
}
Here, by default, we fill in the RequestId; this is necessary if we have an application without an API, but with background jobs. That is when we are the initiators of the request. We also ensure that RequestId is never empty.
Next, we need to populate this class and ensure that this instance is used throughout the entire request processing time. And here, DI will come to our aid. We will simply add it as a Score to our service collection, then we will receive a new class with every request to our service.
services.AddScoped<SessionData>();
Now, we need to read the Request Id from the header. For this, we will use Middleware.
public class RequestIdMiddleware
{
private readonly RequestDelegate _next;
public RequestIdMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, SessionData sessionData)
{
sessionData.RequestId = context.Request.Headers["X-Request-Id"];
await _next(context);
}
}
Fine. Now, let's connect our middleware:
app.UseMiddleware<RequestIdMiddleware>();
To test the work, I will write a simple controller that will return our Request Id and call a service method that will print the Request Id.
[ApiController]
[Route("request_id")]
public class RequestIdController: Controller
{
private readonly SessionData _sessionData;
private readonly RequestIdService _requestIdService;
public RequestIdController(
SessionData sessionData,
RequestIdService requestIdService)
{
_sessionData = sessionData;
_requestIdService = requestIdService;
}
[HttpGet]
public ActionResult<string> Get()
{
_requestIdService.PrintRequestId();
return _sessionData.RequestId;
}
}
And here is our service. Don't forget to add it to the IServiceCollection:
public class RequestIdService
{
private readonly SessionData _sessionData;
public RequestIdService(SessionData sessionData)
{
_sessionData = sessionData;
}
public void PrintRequestId()
{
Console.WriteLine($"RequestIdService request_id: {_sessionData.RequestId}");
}
}
When calling a service with header X-Request-Id = SomeId. Our RequestId SomeId is returned to us + the line RequestIdService request_id: SomeId is displayed in the console.
Super. Now, let’s add it to the header when calling another service. To send messages, I created a helper class HttpSendHelper.
public class HttpSendHelper
{
private readonly HttpClient _httpClient;
private readonly SessionData _sessionData;
public HttpSendHelper(
HttpClient httpClient,
SessionData sessionData)
{
_httpClient = httpClient;
_sessionData = sessionData;
}
public async Task<string> GetRequestAsync(string url, CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Add("X-Request-Id", _sessionData.RequestId);
var response = await _httpClient.SendAsync(request, cancellationToken);
string stringResult = await response.Content.ReadAsStringAsync(cancellationToken);
return stringResult;
}
}
Then I wrote a service that calls the method of our previously written controller. Let's not forget to add it to the IServiceCollection.
public class ExternalApiService
{
private readonly HttpSendHelper _httpSendHelper;
public ExternalApiService(HttpSendHelper httpSendHelper)
{
_httpSendHelper = httpSendHelper;
}
public Task<string> GetRequestIdAsync(CancellationToken cancellationToken)
{
return _httpSendHelper.GetRequestAsync("http://localhost:5000/request_id", cancellationToken);
}
}
Let's add another method to our controller:
[HttpGet("external")]
public Task<string> GetExternal(CancellationToken cancellationToken)
{
return _externalApiService.GetRequestIdAsync(cancellationToken);
}
Now, we need to run our application twice. And call a new method.