使用Relay方式按ID检索实体时出现序列化异常问题
问题
使用HotChocolate结合Relay全局对象标识时,自定义的get{EntityName}查询(simpleQuery)传入Relay风格ID会报错:
No serializer registered for type SchemaInfo
但使用Relay标准的node查询(relayQuery)可以正常返回结果。
两种查询示例:
# 报错的simpleQuery query simpleQuery { getSchemaInfo(Id: "U2NoZW1hSW5mbzq0cC2Zq7keTLzUWk7aem8Y") { name } } # 正常的relayQuery query relayQuery { node(id: "U2NoZW1hSW5mbzq0cC2Zq7keTLzUWk7aem8Y") { ... on SchemaInfo { name } } }
排查到的执行差异
- simpleQuery场景:InputParser的
ParseLiteral方法中,Id字段带有IdFormatter,会触发OptimizedNodeIdSerializer.Parse,但GuidNodeIdValueSerializer对传入的IdType返回IsSupported=false,导致序列化器未初始化,抛出SerializerMissing异常。 - relayQuery场景:InputParser的
ParseLiteral方法中,Id字段无格式化器,直接返回值,因此正常执行。
代码背景
通过程序集动态生成Schema,核心代码如下:
Program.cs
private static void AddSchemaTypes(IRequestExecutorBuilder builder) { // 从程序集中获取所有Entity子类 typeof(Entity).Assembly.GetTypes() .Where(t => t.IsSubclassOf(typeof(Entity))) .ForEach(t => builder // 添加实体类型 .AddType(typeof(EntityType<>).MakeGenericType(t)) // 添加对应查询扩展 .AddTypeExtension(typeof(QueryType<>).MakeGenericType(t))); builder.ConfigureSchema(b => b.AddRootType( new ObjectType(d => d.Name(OperationTypeNames.Query)), OperationType.Query)); } private static void Main(string[] args) { ConfigurationProvider.ConnectionStrings.MustNotBeNull(); DatabaseInitializer.Initialize(ConfigurationProvider.ConnectionStrings.PostgresConnectionString); var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton(new TransactionScopeHandler(new(ConfigurationProvider.ConnectionStrings.PostgresConnectionString))); // 配置GraphQL Server builder.Services.AddGraphQLServer() .AddTransactionScopeHandler(s => s.GetService<TransactionScopeHandler>().MustNotBeNull()) .AddFiltering() .AddSorting() .AddProjections() .ModifyPagingOptions(opt => opt.IncludeTotalCount = true) .AddGlobalObjectIdentification() .AddInMemorySubscriptions(); // 动态添加Schema类型 AddSchemaTypes(builder.Services.AddGraphQL()); var app = builder.Build(); app.UseHttpsRedirection(); app.UseWebSockets(); app.MapGraphQL(); app.RunWithGraphQLCommands(args); }
EntityType
internal class EntityType<T> : ObjectType<T> where T : Entity, new() { protected override void Configure(IObjectTypeDescriptor<T> descriptor) { descriptor.MustNotBeNull(); descriptor.Description("description"); descriptor .ImplementsNode() .IdField(f => f.Id) .ResolveNode(Query.GetAsync<T>); typeof(T).GetProperties().ForEach(property => { var prop = descriptor .Field(property) .Description(property.Name); if (property.Name is nameof(Entity.Id)) { prop.ID(); } }); } }
QueryType
internal class QueryType<T> : ObjectTypeExtension where T : Entity, new() { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Name(OperationTypeNames.Query); descriptor .Field($"get{typeof(T).Name}") .Description("some description") .Type<EntityType<T>>() .Resolve(async context => await Query.GetAsync<T>(context, default!)) .Argument(nameof(Entity.Id), a => a .Description("some description") .Type<IdType>() .ID<T>()); descriptor .Field($"get{typeof(T).Name}Collection") .Description("some description") .Type<ListType<EntityType<T>>>() .Resolve(Query.Get<T>) .UseOffsetPaging() .UseProjection() .UseFiltering() .UseSorting(); } }
Entity基类
public abstract class Entity { public Guid Id { get; set; } public DateTime? Timestamp { get; set; } }
解决方案
1. 修正GraphQL配置的类型添加顺序
当前代码中,AddSchemaTypes使用builder.Services.AddGraphQL()创建了新的IRequestExecutorBuilder,导致动态添加的类型没有和之前配置了AddGlobalObjectIdentification的服务器实例关联,无法注册对应的节点序列化器。
修改Main方法,将AddGraphQLServer返回的builder传入AddSchemaTypes:
private static void Main(string[] args) { ConfigurationProvider.ConnectionStrings.MustNotBeNull(); DatabaseInitializer.Initialize(ConfigurationProvider.ConnectionStrings.PostgresConnectionString); var builder = WebApplication.CreateBuilder(args); builder.Services.AddHttpContextAccessor(); builder.Services.AddSingleton(new TransactionScopeHandler(new(ConfigurationProvider.ConnectionStrings.PostgresConnectionString))); // 获取GraphQL Server的builder实例 var graphQlBuilder = builder.Services.AddGraphQLServer() .AddTransactionScopeHandler(s => s.GetService<TransactionScopeHandler>().MustNotBeNull()) .AddFiltering() .AddSorting() .AddProjections() .ModifyPagingOptions(opt => opt.IncludeTotalCount = true) .AddGlobalObjectIdentification() .AddInMemorySubscriptions(); // 使用同一个builder添加动态类型 AddSchemaTypes(graphQlBuilder); var app = builder.Build(); app.UseHttpsRedirection(); app.UseWebSockets(); app.MapGraphQL(); app.RunWithGraphQLCommands(args); }
2. 简化QueryType的参数配置
ID<T>()扩展方法已经会自动将参数类型设置为Relay兼容的ID类型,并关联对应的序列化器,无需额外指定.Type<IdType>(),移除该配置避免冲突:
internal class QueryType<T> : ObjectTypeExtension where T : Entity, new() { protected override void Configure(IObjectTypeDescriptor descriptor) { descriptor.Name(OperationTypeNames.Query); descriptor .Field($"get{typeof(T).Name}") .Description("some description") .Type<EntityType<T>>() .Resolve(async context => await Query.GetAsync<T>(context, default!)) .Argument(nameof(Entity.Id), a => a .Description("some description") // 移除Type<IdType>(),保留ID<T>()即可 .ID<T>()); // ... 集合查询配置不变 } }
3. 验证序列化器注册
修改后,HotChocolate会在启动时为所有动态添加的Entity子类注册对应的NodeIdSerializer,此时simpleQuery中的Relay ID会被正确解析为Guid,即可正常返回结果。
内容的提问来源于stack exchange,提问作者Vassiliy Bychko
相关产品推荐
相关产品推荐

