who-aditya-nawandar
who-aditya-nawandar

Reputation: 1242

How to aggregate requests from multiple microservices in the ApiGateway?

I have microservices architecture with an API Gateway

ApiGateway controller:

namespace ApiGateway.Controllers.Aggregators
{
    [ApiController]
    [Route("aggregator/[controller]")]
    public class EmployeeTrainingsController : ControllerBase
    {
        private readonly HttpClient _httpClient;

        public EmployeeTrainingsController(IHttpClientFactory httpClientFactory)
        {
            _httpClient = httpClientFactory.CreateClient();
        }

        [HttpGet("employeetrainings")]
        public async Task<IActionResult> GetEmployeeTrainings()
        {
            try
            {
                Console.WriteLine("GetEmployeeTrainings in aggregator called...");
                // Check if the TenantId header exists
                Console.WriteLine($"Incoming Headers: {string.Join(", ", Request.Headers.Select(h => $"{h.Key}: {h.Value}"))}");

                if (!Request.Headers.TryGetValue("X-Tenant-ID", out var tenantIdHeader))
                {
                    Console.WriteLine("X-Tenant-ID missing in request.");
                    return BadRequest("TenantId header is missing.");
                }

                // Parse the TenantId to ensure it's a valid GUID
                if (!Guid.TryParse(tenantIdHeader, out var tenantId))
                {
                    return BadRequest("Invalid TenantId.");
                }

                // Console.WriteLine($"Forwarding request to PeopleService with TenantId: {tenantId}");
                            // // Set the TenantId header in the requests to the downstream services
                            _httpClient.DefaultRequestHeaders.Remove("X-Tenant-ID"); // Ensure no duplicates
                            _httpClient.DefaultRequestHeaders.Add("X-Tenant-ID", tenantId.ToString());
                            // Fetch data from multiple services
 /// GETTING A 500 HERE...
            var employeeTrainingsResponse = await _httpClient.GetAsync("http://localhost:3000/people/employeetrainings/allemployeetrainings");
            
            var trainingResponse = await _httpClient.GetAsync("http://localhost:3000/safety/trainings");

            if (!employeeTrainingsResponse.IsSuccessStatusCode || !trainingResponse.IsSuccessStatusCode)
            {
                return StatusCode(500, new
                {
                    Message = "Failed to fetch data from one or more services",
                    Details = new
                    {
                        EmployeeTrainingsStatus = employeeTrainingsResponse.StatusCode,
                        TrainingStatus = trainingResponse.StatusCode
                    }
                });

            }
                            var employeetrainingss = await employeeTrainingsResponse.Content.ReadFromJsonAsync<List<EmployeeTrainingsDto>>();
                            if (employeetrainingss == null)
                            {
                                return StatusCode(500, "Failed to deserialize employee-trainings data.");
                            }

                            var trainings = await trainingResponse.Content.ReadFromJsonAsync<List<TrainingDto>>();
                            if (trainings == null)
                            {
                                return StatusCode(500, "Failed to deserialize training data.");
                            }

                            // Combine data to create a table-like structure

                            return Ok(result);
                        }

                        catch (Exception ex)
                        {
                            Console.WriteLine($"Error: {ex.Message}");
                            return StatusCode(StatusCodes.Status500InternalServerError, new { Error = ex.Message });
                        }
                    }

                }
            }

The gateway runs on port 3000.

The direct call to the microservices endpoints works.

YARP is also configured correctly as far as I understand:

                                {
                              "Logging": {
                                "LogLevel": {
                                  "Default": "Debug",
                                  "Microsoft.AspNetCore": "Warning",
                                  "Yarp.ReverseProxy": "Debug"
                                }
                              },
                              "Auth0": {
                                "Domain": "dev-agami-cmms.us.auth0.com",
                                "ClientId": "OSeIEeytxUeDkinUcAdAyGMLQmnyBV1d",
                                "ClientSecret": "w8AVyQsiixlnbousjOCKLKEK_TBLryuK9joKpNs_CDKGLEPjjNWBtG1MX5xmZZDe",
                                "Audience": "https://localhost/"
                              },
                              "ReverseProxy": {},
                              "Routes": {
                                "people": {
                                  "ClusterId": "PeopleService",
                                  "Match": {
                                    "Path": "/people/{**catchAll}"
                                  },
                                  "Transforms": [
                                    {
                                      "RequestHeader": "X-Tenant-ID",
                                      "Set": "{X-Tenant-ID}"
                                    },
                                    {
                                      "PathPattern": "{**catchAll}"
                                    },
                                    {
                                      "RequestHeaderOriginalHost": "true"
                                    }
                                  ]
                                },
                                "safety": {
                                  "ClusterId": "SafetyService",
                                  "Match": {
                                    "Path": "/safety/{**catchAll}"
                                  },
                                  "Transforms": [
                                    {
                                      "RequestHeader": "X-Tenant-ID",
                                      "Set": "{X-Tenant-ID}"
                                    },
                                    {
                                      "PathPattern": "{**catchAll}"
                                    },
                                    {
                                      "RequestHeaderOriginalHost": "true"
                                    }
                                  ]
                                },
                                "tenant": {
                                  "ClusterId": "TenantService",
                                  "Match": {
                                    "Path": "/api/tenants/{**catchAll}"
                                  }
                                },
                                "shared": {
                                  "ClusterId": "SharedService",
                                  "Match": {
                                    "Path": "/shared/{**catchAll}"
                                  }
                                }
                              },
                              "Clusters": {
                                "PeopleService": {
                                  "Destinations": {
                                    "PeopleService1": {
                                      "Address": "http://localhost:5002/"
                                    }
                                  }
                                },
                                "SafetyService": {
                                  "Destinations": {
                                    "SafetyService1": {
                                      "Address": "http://localhost:5003/"
                                    }
                                  }
                                },
                                "TenantService": {
                                  "Destinations": {
                                    "TenantService1": {
                                      "Address": "http://localhost:6000/"
                                    }
                                  }
                                },
                                "SharedService": {
                                  "Destinations": {
                                    "SharedService1": {
                                      "Address": "https://localhost:5007/"
                                    }
                                  }
                                }
                              }
                            }

Gateway Program.cs

                using ApiGateway.Middleware;
                using ApiGateway.Services;
                using PeopleService.Services;

            var builder = WebApplication.CreateBuilder(args);

            // 1) Add controllers
            builder.Services.AddControllers();

            // 2) Add YARP reverse proxy
            builder.Services.AddReverseProxy()
                .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

            // 3) Named client for aggregator calls 
            //builder.Services.AddHttpClient("AggregatorClient");
            // builder.Services.AddHttpClient("AggregatorClient", client =>
            // {
            //     // Api Gateway Endpoint
            //     client.BaseAddress = new Uri("http://localhost:3000");
            // });

            // 4) TenantServiceClient for tenant validation (direct call to TenantService)
            builder.Services.AddHttpClient<TenantServiceClient>(client =>
            {
                // If TenantService is at http://localhost:6000
                client.BaseAddress = new Uri("http://localhost:6000");
            });

            builder.Logging.AddConsole(options =>
            {
                options.IncludeScopes = true;
                options.TimestampFormat = "[yyyy-MM-dd HH:mm:ss] ";
            });

            var app = builder.Build();

            // 6) (Optional) If you want HTTPS redirection for production
            app.UseHttpsRedirection();

            // 7) Insert the TenantMiddleware before routing/proxying
            app.UseMiddleware<TenantMiddleware>();

            // 8) Enable routing, then map the reverse proxy and controllers
            app.UseRouting();
            app.Use(async (context, next) =>
            {
                Console.WriteLine($"Incoming Request: {context.Request.Method} {context.Request.Path}");
                await next();
                Console.WriteLine($"Outgoing Response: {context.Response.StatusCode}");
            });
            app.MapReverseProxy();
            app.MapReverseProxy();      // YARP routes
            app.MapControllers();       // Custom aggregator or fallback routes

            app.Run();

I can provide any other code required.

Upvotes: 0

Views: 29

Answers (1)

I think the root issue is with your YARP header transform settings. If you're not experiencing any problems when calling PeopleService directly, then the problem likely lies in how your API Gateway handles the request headers.

Here’s the breakdown:

  • In your Aggregator Controller, you receive the X-Tenant-ID header, validate that it's a proper GUID, and then add it to your HttpClient for downstream calls.

  • However, in your YARP configuration you have a transform like this:

    "Transforms": [
      {
        "RequestHeader": "X-Tenant-ID",
        "Set": "{X-Tenant-ID}"
      }
    ]
    
  • The issue here is that {X-Tenant-ID} is meant to be used as a route parameter token. Since you’re already including the X-Tenant-ID header via HttpClient, YARP ends up overwriting it with the literal value "{X-Tenant-ID}".

  • This results in PeopleService receiving an incorrect header value (not a valid GUID), which causes the 500 error.

The simplest solution is to remove the transform that meddles with the X-Tenant-ID header. Your configuration should just forward the header as is. For example, you can update your configuration to:

"Transforms": [
  {
    "PathPattern": "{catchAll}"
  },
  {
    "RequestHeaderOriginalHost": "true"
  }
]

In short, let the X-Tenant-ID header come from your controller without YARP interfering. This should ensure that PeopleService gets the correct header value, and your 500 error should go away.

Hope that helps!

Upvotes: 0

Related Questions