You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 06:17:33