如何用HotChocolate合并两个同Schema服务实现API组合网关?
问题:HotChocolate网关合并多同Schema服务的查询结果
我想用HotChocolate搭建API组合网关,拼接多个服务的GraphQL接口。当各服务Schema不同时运行正常,但遇到两个服务提供相同Schema的场景(比如两个图书服务都提供Book Schema),希望网关把Book Schema的请求转发到所有服务并合并结果,但目前HotChocolate只会调用其中一个Books端点。
现有代码
服务端配置
var builder = WebApplication.CreateBuilder(); builder.Services.AddGraphQLServer() .AddRemoteSchema(WellKnownSchemaNames.Books) .AddRemoteSchema(WellKnownSchemaNames.Authors) .AddTypeExtensionsFromFile("./Stitching.graphql"); builder.Services.AddHttpClient(WellKnownSchemaNames.Books, c => c.BaseAddress = new Uri("http://localhost:7011/graphql")); builder.Services.AddHttpClient(WellKnownSchemaNames.Authors, c => c.BaseAddress = new Uri("http://localhost:7012/graphql")); // 注意此客户端Schema相同但端口不同 builder.Services.AddHttpClient(WellKnownSchemaNames.Books, c => c.BaseAddress = new Uri("http://localhost:7013/graphql")); var app = builder.Build(); app.MapGraphQL(); app.Run($"http://localhost:7010");
Book的Schema与查询逻辑
public class Book { public string Id { get; set; } public string Title { get; set; } } public class BooksQuery { public static Book[] _books; // 初始化时设置,两个端点的值不同 public Book[] Books => _books; public Book GetBook(string id) => _books.Single(b => b.Id == id); }
期望行为
books:查询两个端点,合并结果后返回book(id: "x"):查询两个端点,若任意端点返回非错误结果则返回该结果
解决方案
问题出在重复添加同名的RemoteSchema,HotChocolate会覆盖之前的配置,只会保留最后一个。要实现多服务结果合并,需要给每个服务分配唯一标识,再通过自定义解析器处理请求转发和结果合并。
1. 配置唯一命名的RemoteSchema和HttpClient
给每个图书服务设置独立的Schema名称,避免覆盖:
var builder = WebApplication.CreateBuilder(); builder.Services.AddGraphQLServer() .AddRemoteSchema("BooksService1") .AddRemoteSchema("BooksService2") .AddRemoteSchema(WellKnownSchemaNames.Authors) .AddTypeExtensionsFromFile("./Stitching.graphql") .AddResolver<BookResolvers>(); // 注册自定义解析器 // 为每个服务配置独立HttpClient builder.Services.AddHttpClient("BooksService1", c => c.BaseAddress = new Uri("http://localhost:7011/graphql")); builder.Services.AddHttpClient("BooksService2", c => c.BaseAddress = new Uri("http://localhost:7013/graphql")); builder.Services.AddHttpClient(WellKnownSchemaNames.Authors, c => c.BaseAddress = new Uri("http://localhost:7012/graphql")); var app = builder.Build(); app.MapGraphQL(); app.Run($"http://localhost:7010");
2. 编写类型扩展文件(Stitching.graphql)
定义网关对外暴露的查询字段,并关联自定义解析器:
extend type Query { books: [Book!]! @resolver(name: "GetBooksAsync") book(id: String!): Book @resolver(name: "GetBookByIdAsync") } type Book @extends { id: String! title: String! } extend schema @link( url: "https://specs.apollo.dev/federation/v2.0" import: ["@extends"] )
3. 实现自定义解析器
编写C#类处理多服务调用和结果合并:
public class BookResolvers { private readonly IQueryExecutor _queryExecutor; public BookResolvers(IQueryExecutor queryExecutor) { _queryExecutor = queryExecutor; } // 合并两个服务的books结果 public async Task<Book[]> GetBooksAsync() { var serviceNames = new[] { "BooksService1", "BooksService2" }; var tasks = serviceNames.Select(async name => { var response = await _queryExecutor.ExecuteAsync(new QueryRequest { Query = "{ books { id title } }", SchemaName = name }); return response.Data.GetFieldValue<Book[]>("books"); }); var allBooks = await Task.WhenAll(tasks); return allBooks.SelectMany(books => books).ToArray(); } // 查询两个服务,返回第一个非空结果 public async Task<Book?> GetBookByIdAsync(string id) { var serviceNames = new[] { "BooksService1", "BooksService2" }; foreach (var name in serviceNames) { var response = await _queryExecutor.ExecuteAsync(new QueryRequest { Query = "{ book(id: $id) { id title } }", Variables = new Dictionary<string, object> { { "id", id } }, SchemaName = name }); var book = response.Data.GetFieldValue<Book?>("book"); if (book != null) { return book; } } return null; } }
内容的提问来源于stack exchange,提问作者EJHewy
相关产品推荐
相关产品推荐

