ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.03k stars 723 forks source link

Stitching schemas with custom scalars #3067

Closed gao-artur closed 3 years ago

gao-artur commented 3 years ago

We are considering to migrate from graphql-dotnet to hot-chocolate. To make this process less painful we are planning to use stitching feature and stitch graphql-dotnet schema (as remote) with hot-chocolate schema (as local) running in same process. Then we will move our graph types one by one from one framework to another. For that I created POC project that works fine until I add custom scalar to downstream schema. I get this exception:

HotChocolate.SchemaException: For more details look at the `Errors` property.

      1. Unable to resolve type reference `Output: CustomScalar`. (HotChocolate.Types.ObjectType)

         at HotChocolate.Configuration.TypeCompletionContext.GetType[T](ITypeReference typeRef)
         at HotChocolate.Types.FieldBase`2.OnCompleteField(ITypeCompletionContext context, TDefinition definition)
         at HotChocolate.Types.OutputFieldBase`1.OnCompleteField(ITypeCompletionContext context, TDefinition definition)
         at HotChocolate.Types.ObjectField.OnCompleteField(ITypeCompletionContext context, ObjectFieldDefinition definition)
         at HotChocolate.Types.FieldBase`2.CompleteField(ITypeCompletionContext context)
         at HotChocolate.Types.FieldInitHelper.CompleteFields[TTypeDef,TFieldType,TFieldDef](ITypeCompletionContext context, TTypeDef definition, IReadOnlyCollection`1 fields)
         at HotChocolate.Types.ObjectType.OnCompleteType(ITypeCompletionContext context, ObjectTypeDefinition definition)
         at HotChocolate.Types.TypeSystemObjectBase`1.CompleteType(ITypeCompletionContext context)
         at HotChocolate.Configuration.TypeInitializer.<CompleteTypes>g__CompleteType|32_0(RegisteredType registeredType)
         at HotChocolate.Configuration.TypeInitializer.ProcessTypes(TypeDependencyKind kind, Func`2 action)
         at HotChocolate.Configuration.TypeInitializer.CompleteTypes()
         at HotChocolate.Configuration.TypeInitializer.Initialize(Func`1 schemaResolver, IReadOnlySchemaOptions options)
         at HotChocolate.SchemaBuilder.Setup.InitializeTypes(SchemaBuilder builder, DescriptorContext context, IBindingLookup bindingLookup, IReadOnlyList`1 types, LazySchema lazySchema)
         at HotChocolate.SchemaBuilder.Setup.Create(SchemaBuilder builder)
         at HotChocolate.SchemaBuilder.Create()
         at HotChocolate.SchemaBuilder.HotChocolate.ISchemaBuilder.Create()
         at HotChocolate.Execution.RequestExecutorResolver.CreateSchemaAsync(NameString schemaName, RequestExecutorSetup options, IServiceProvider serviceProvider, CancellationToken cancellationToken)
         at HotChocolate.Execution.RequestExecutorResolver.CreateSchemaServicesAsync(NameString schemaName, RequestExecutorSetup options, CancellationToken cancellationToken)
         at HotChocolate.Execution.RequestExecutorResolver.GetRequestExecutorNoLockAsync(NameString schemaName, CancellationToken cancellationToken)
         at Microsoft.Extensions.DependencyInjection.HotChocolateStitchingRequestExecutorExtensions.<>c__DisplayClass5_0.<<AddRemoteSchema>b__2>d.MoveNext()
      --- End of stack trace from previous location ---
         at HotChocolate.Execution.RequestExecutorResolver.CreateSchemaAsync(NameString schemaName, RequestExecutorSetup options, IServiceProvider serviceProvider, CancellationToken cancellationToken)
         at HotChocolate.Execution.RequestExecutorResolver.CreateSchemaServicesAsync(NameString schemaName, RequestExecutorSetup options, CancellationToken cancellationToken)
         at HotChocolate.Execution.RequestExecutorResolver.GetRequestExecutorNoLockAsync(NameString schemaName, CancellationToken cancellationToken)
         at HotChocolate.Execution.RequestExecutorResolver.GetRequestExecutorAsync(NameString schemaName, CancellationToken cancellationToken)
         at HotChocolate.Execution.RequestExecutorProxy.GetRequestExecutorAsync(CancellationToken cancellationToken)
         at HotChocolate.AspNetCore.HttpPostMiddleware.HandleRequestAsync(HttpContext context, AllowedContentType contentType)
         at HotChocolate.AspNetCore.HttpPostMiddleware.InvokeAsync(HttpContext context)
         at HotChocolate.AspNetCore.WebSocketSubscriptionMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at GraphQL.Server.Transports.AspNetCore.GraphQLHttpMiddleware`1.InvokeAsync(HttpContext context) in /_/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs:line 50
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

Version Info: Dotnet 5.0 GraphQL-Dotnet v4.4.1 HotChocolate v11.0.9

The project to reproduce the issue can be found here: https://github.com/gao-artur/graphql-stitching

michaelstaib commented 3 years ago

Hi there can you hop on the slack channel? you get much better response there. We can walk you through the custom scalars.... At the moment we kind of have to declare them. We will remove this requirement with the May update which kind of will make this plug and play. For now join us on slack and post the issue to general.

michaelstaib commented 3 years ago

We checked this on slack.

The project had two issues.

  1. The Hot Chocolate scalar was called customScalar and the GraphQL-DotNet scalar was called CustomScalar. GraphQL type names are case sensitive so the stitching layer was unable to merge the types.
  2. Custom Scalars need to be substituted for remote schemas, this can be done by adding a configuration to the named internal stub schema like the following:
services
    .AddGraphQLServer("RemoteSchemaName")
    .AddType(new AnyType("CustomScalar")); <--- substitution

Version 12 will automatically substitute unknown scalars.

Discussion on slack: https://hotchocolategraphql.slack.com/archives/CD9TNKT8T/p1617793130175200

zliebersbach commented 2 years ago

@michaelstaib this is still an issue in the v12 prerelease and the provided workaround doesn't work...

zliebersbach commented 2 years ago

Tried with:

graphqlBuilder.AddRemoteSchema(id)
    .AddType<EmailAddressType>();
joshclaxton commented 2 years ago

Found this solution on the Slack channel. you have to add the substitution to both the Local Schema and Remote Schema. https://hotchocolategraphql.slack.com/archives/CN3EZ03U1/p1625638621159500

var graphqlBuilder = services.AddGraphQLServer();
            graphqlBuilder.AddRemoteSchema(("RemoteAPI", ignoreRootTypes: true)
                .Services
                .AddGraphQL("RemoteAPI")
                // this is an edge case with scalars, but not new object Types
                // we must add the Scalar to BOTH the remote schema and the Local schema
                // see #ScalarEdgeCase
                .AddType(new AnyType("GuidGraphType")); 

            graphqlBuilder.AddQueryType<Query>()
                // this is an edge case with scalars, but not new object Types
                // we must add the Scalar to BOTH the remote schema and the Local schema
                // see #ScalarEdgeCase
                .AddType(new AnyType("GuidGraphType"))
                .AddTypeExtensionsFromFile("./Stitching.graphql")

That said, I would really like to see us not have to do this. It's great to have Types come in automagically. Curious why scalars are different.

stephenaggett commented 2 years ago

This workaround doesn't work for me - I get "The name delegate was already registered by another type. (HotChocolate.Stitching.DelegateDirectiveType)" with the code:

requestExecutorBuilder.AddRemoteSchema(remoteSchemaName, ignoreRootTypes: true)
                .Services.AddGraphQL(remoteSchemaName)
                .AddType<NonEmptyStringType>()
                .AddType<PositiveIntType>()
                .AddType<NonNegativeIntType>();

When there is more than one remote schema present. The same types were added in the domain service and the gateway service schema configuration too. I think this issue should not be closed, as a true resolution would be that manually specifying the additional scalars present in the remote schema is unnecessary (as @joshclaxton said).

stephenaggett commented 2 years ago

@PascalSenn Updating to v12 didn't work either. I've been trying to resolve this issue for two days now and I can't afford to expend any more time on it so I have removed all use of HC scalar types (NonEmptyString, PositiveInt, NonNegativeInt etc) in downstream domain services to allow the gateway service to run without exceptions. Please can you extend your test coverage to include use of HC scalar types in federated/stitched schemas so you can get this sorted. Thanks.

joshclaxton commented 2 years ago

@stephenaggett After playing with this, we decided it wasn't worth the hard dependency and requirement of updating both sites anyway.... We now have a rule that our graphs ONLY use scalars in the GraphQL spec. If HC has this issue, it's possible other libraries do as well. We want to make sure our Clients (public or internal) do not have conflicts or hard dependencies like this.

Of course, this is an imperfect solution but maybe it's a healthy long term mindset.

gao-artur commented 2 years ago

@stephenaggett @joshclaxton it seems stitching refactoring was pushed to v13 https://github.com/ChilliCream/hotchocolate/projects/28#column-13115712

michaelstaib commented 2 years ago

We split the release. We are on the last stretch for V12 and start soon work on V13 which focuses on stitching. We have started already planing work for the new stitching engine.

mickdelaney commented 2 years ago

So for v12, custom scalars are not supporting in stitching ? I just want to avoid wasting time trying to fix it

would be good to have this sort of thing in an FAQ.

Euphoric commented 2 years ago

Just hit this issue. Why is it closed when it wasn't solved yet?

nibblesnbits commented 2 years ago

Same. Is there a real solution to this?

PascalSenn commented 2 years ago

@nibblesnbits They are supported in 12 but you have to add it to the schema and to the internal schema of the downstream service on the gateway. So, what this means is basically:

⚠️ IMPORTANT: This is all gateway code

services
    .AddGraphQLServer()
    .AddQueryType(d => d.Name("Query"))
    .AddRemoteSchema("Accounts")
    .AddRemoteSchema("Reviews")
    .AddType(new AnyType("CustomScalarFromAccounts"))
    .AddType(new AnyType("CustomScalarFromReviews"));

services
    .AddGraphQL("Accounts")
    .AddType(new AnyType("CustomScalarFromAccounts"));

services
    .AddGraphQL("Reviews")
    .AddType(new AnyType("CustomScalarFromReviews"));
PascalSenn commented 2 years ago

In v13 thei will all happen automatically

nibblesnbits commented 2 years ago

@PascalSenn, That works! But here's a new issue....

fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      HotChocolate.SchemaException: For more details look at the `Errors` property.

      1. The name `Node` was already registered by another type. (HotChocolate.Types.InterfaceType)

         at HotChocolate.Configuration.TypeRegistry.Register(NameString typeName, RegisteredType registeredType)

Both of my APIs need to include this type. How do I ignore one of them? I've tried IgnoreType in various ways already.

NOTE: Issue resolved. I had to specify the schema name in IgnoreType.

PBTests commented 1 year ago

In v13 thei will all happen automatically

I assume this is not currently accurate?

matafonoff commented 1 year ago

Having the same issue with scalars on 13.0.5

EvilVir commented 1 year ago

Same here: Unable to resolve type reference ReferenceOfContainerElement. (HotChocolate.Types.InterfaceType).

Where ReferenceOfContainerElement is scalar from remote schema. I tried to use .AddType<> on gateway side to add that scalar but no go. When any .AddRemoteSchema* method is executed then things like .AddType or .AddTypeRewriter doesn't even get called.

Any help please? I know there's some "Fusion" around the corner, but it's still undocummented beta and "Stitching" package is advertised as working and supported HotChocolate's element.

andreiZi commented 8 months ago

Having the same issue. When connecting to my graphql-dotnetmicroservice I get Unable to resolve type reference DateTimeOffset!. (HotChocolate.Types.ObjectType)

I use 13.5.1 btw

mderriey commented 2 weeks ago

I'm using 13.9.4, and it took me a large number of attempts to get this working.

Our scenario is we have a GraphQL API that covers 90% of our needs, so our hope is to have a separate Hot Chocolate project that stitches the schema from the existing GraphQL API with specific, locally-defined operations from a separate schema.

One issue we faced is the custom scalars one, which was solved following https://github.com/ChilliCream/graphql-platform/issues/3067#issuecomment-1121655104, with a small tweak.

The other one is with the fact that we were hoping Hot Chocolate would merge the Query types of both schemas, which doesn't seem to be feasible. The workaround here was to define the "local" query type as an extension of the remote query type.

All in all, it looks like this:

public static class WellKnownSchemaNames
{
    public const string PublicApi = nameof(PublicApi);
}
builder.Services
    .AddHttpClient(WellKnownSchemaNames.PublicApi, client => client.BaseAddress = new("http://localhost:4000/graphql"));

// List of custom scalars we need to register with Hot Chocolate to avoid the "Unable to resolve type reference" error
string[] customScalars = [
    "MyCustomScalar1",
    "MyCustomScalar2",
];

var stitchedSchemaExecutorBuilder = builder.Services
    .AddGraphQLServer()
    // Add the remote schema
    .AddRemoteSchema(WellKnownSchemaNames.PublicApi)
    // Add the local query type, but not as a query type otherwise we run into an error
    .AddType<Query>();

// Get a reference of the public API schema to register
var publicApiSchemaExecutorBuilder = builder.Services.AddGraphQL(WellKnownSchemaNames.PublicApi);

foreach (var customScalar in customScalars)
{
    // I'm not sure why, but we had to register the custom scalars with both the remote schema and the stitched schema that references the remote one.
    publicApiSchemaExecutorBuilder.AddType(new AnyType(customScalar));
    stitchedSchemaExecutorBuilder.AddType(new AnyType(customScalar));
}

Finally, the Query type from the local schema looks like:

[ExtendObjectType("Query")]
public class Query
{
    public string LocalOperation() => "This is from the local GraphQL schema";
}

If there's a better way to do this, I'd be very happy to know about it.