graphql-dotnet/server

Missing classes while upgrading to GraphQL.Server.All from GraphQL.Server.* libraries

vinaygangaraj opened this issue · 11 comments

Hi @Shane32, @sungam3r

We are migrating our Asp.netcore 3.1 web application to .net 6. As a part of this migration we are also planning to upgrade GraphQL.Server.* libraries.

As mentioned in the ASP.NET Core GraphQL Server readme file (https://github.com/graphql-dotnet/server/tree/f0512031dc99e4bb68e7e4fcf40084a8eeca7450#readme) I un-installed the packages which are not required.

Packages used in AspNetCore 3.1:
GraphQL.Authorization Version="4.0.0"
GraphQL.Server.Core Version="5.2.0"
GraphQL.Server.Transports.AspNetCore Version="5.2.0"
GraphQL.Server.Transports.AspNetCore.NewtonsoftJson Version="5.2.0"
GraphQL.Server.Transports.AspNetCore.SystemTextJson Version="5.2.0"
GraphQL.Server.Transports.Subscriptions.Abstractions Version="5.2.0"
GraphQL.Server.Transports.Subscriptions.WebSockets Version="5.2.0"
GraphQL.Server.Ui.Playground Version="5.2.0"
GraphQL.Server.Ui.Voyager Version="5.2.0"

Packages currently used for .NET 6:
GraphQL.Authorization Version="7.0.0"
GraphQL.Server.All Version="7.5.0"

After these libraries upgradation/installation, I faced a few breaking changes in my repository which I was able to fix it by going through your release documentations.

But, I am facing couple of issues with below 2 code snippets as I am not able to figure out the alternative code changes with the latest library upgrade. Please help me on fixing these issues.

  1. In below code DefaultGraphQLExecuter and GraphQLOptions are not present with latest GraphQL.Server.All library.
public static IServiceCollection AddCustomGraphQL(this IServiceCollection services,
                                                     IWebHostEnvironment hostingEnvironment, IConfiguration configuration) =>
services.AddTransient(typeof(IGraphQLExecuter<>), typeof(InstrumentingGraphQLExecutor<>));
public class InstrumentingGraphQLExecutor<TSchema> : DefaultGraphQLExecuter<TSchema>
    where TSchema : ISchema
  {
    private readonly GraphQLOptions options;

    public InstrumentingGraphQLExecutor(
      TSchema schema,
      IDocumentExecuter documentExecuter,
      IOptions<GraphQLOptions> options,
      IEnumerable<IDocumentExecutionListener> listeners,
      IEnumerable<IValidationRule> validationRules)
      : base(schema, documentExecuter, options, listeners, validationRules) =>
      this.options = options.Value;

    public override async Task<ExecutionResult> ExecuteAsync(
      string operationName,
      string query,
      Inputs variables,
      IDictionary<string, object> context,
      IServiceProvider requestServices,
      CancellationToken cancellationToken = default)
    {
      var result = await base.ExecuteAsync(operationName, query, variables, context, requestServices, cancellationToken);
      if (this.options.EnableMetrics)
      {
        result.EnrichWithApolloTracing(DateTime.UtcNow);
      }
      return result;
    }

    protected override ExecutionOptions GetOptions(
      string operationName,
      string query,
      Inputs variables,
      IDictionary<string, object> context,
      IServiceProvider requestServices,
      CancellationToken cancellationToken)
    {
      var option = base.GetOptions(operationName, query, variables, context, requestServices, cancellationToken);

      if (this.options.EnableMetrics)
      {
        option.Schema.FieldMiddleware.Use(new InstrumentFieldsMiddleware());
      }
      option.ValidationRules = DocumentValidator.CoreRules.Concat(new[] { NoIntrospection.Instance });
      return option;
    }
  }
  1. In below code IOperationMessageListener and MessageHandlingContext are not present with latest GraphQL.Server.All library
public IServiceProvider ConfigureServices(IServiceCollection services) =>
	services.AddTransient<IOperationMessageListener, AuthorizationListener>();
public class AuthorizationListener : IOperationMessageListener
  {
    private readonly IHttpContextAccessor httpContextAccessor;

    public AuthorizationListener(IHttpContextAccessor contextAccessor) => httpContextAccessor = contextAccessor;

    public Task BeforeHandleAsync(MessageHandlingContext context)
    {
      if (context.Message.Type == MessageType.GQL_CONNECTION_INIT)
      {
        //some code
      }
      return Task.CompletedTask;
    }

    public Task HandleAsync(MessageHandlingContext context) => Task.CompletedTask;
    public Task AfterHandleAsync(MessageHandlingContext context) => Task.CompletedTask;
  }

DefaultGraphQLExecuter and GraphQLOptions are not present with latest GraphQL.Server.All library.

IGraphQLExecuter<TSchema> was replaced with IDocumentExecuter<TSchema>. See the default implementation here.

However, your code can likely be replaced with a call to .UseApolloTracing such as this snippet:

services.AddGraphQL(b => b
    // other configuration stuff here

    // always-on sample
    .UseApolloTracing()

    // sample controlled by appsettings.json
    .UseApolloTracing(opts => {
        var myOptions = opts.RequestedServices.GetRequiredService<IOptions<MyAppConfig>>().Value;
        return myOptions.EnableApolloTracing;
    })
);

In addition to a custom implementation of IDocumentLExecuter<TSchema>, there are two other ways to implement custom code such as your prior implementation:

  1. Within the AddGraphQL call, call ConfigureExecution. This approach can be layered, and is suggested over a custom implementation of IDocumentExecuter<TSchema>. See https://graphql-dotnet.github.io/docs/migrations/migration7/#4-add-code-classlanguage-textconfigureexecutioncode-builder-method-added-in-530

  2. Override one or methods in the middleware (such as ExecuteRequestAsync) - see https://github.com/graphql-dotnet/server#graphqlhttpmiddleware . But again, ConfigureExecution is recommended.

IOperationMessageListener and MessageHandlingContext are not present with latest GraphQL.Server.All library

While there is no direct replacement for the prior code, there is a new interface IWebSocketAuthenticationService for authorization of Websocket requests. See: https://github.com/graphql-dotnet/server#authentication-for-websocket-requests for a sample code snippet.

There is also a complete sample of JWT authorization including for subscription requests:

I would like to note that .UseApolloTracing() always installs InstrumentFieldsMiddleware. This is because when you have a singleton schema (which is recommended), the field middleware is built with the schema when the schema is initialized, which only occurs once. The UseApolloTracing method does not know if delegate passed to it (which executes at runtime) will be returning true or false so it has to assume that it may be true. So, the field middleware is always installed.

If you have a scoped schema, there is a potential performance enhancement available -- but changing to a singleton schema would be MUCH more effective.

And if you can make the determination within your Startup.cs code, you can simply use an if block around the call to UseApolloTracing() as shown below to prevent the middleware from being installed:

// make determination in any way; this demonstrates using compile-time flag
#if DEBUG
var enableApolloTracing = false;
#else
var enableApolloTracing = true;
#endif

services.AddGraphQL(b => {
    // other configuration stuff here

    if (enableApolloTracing)
    {
        b.UseApolloTracing();
    }
});

Hi @Shane32 ,

Thanks for your quick response and suggestions. For ApolloTracing issue, I have done the below changes based on your suggestions by using UseApolloTracing().

public static IServiceCollection AddCustomGraphQL(this IServiceCollection services,
                                                     IWebHostEnvironment hostingEnvironment, IConfiguration configuration) =>
services.AddGraphQL(
         (options) =>
         {
           options.UseApolloTracing(x =>
           {
             x.EnableMetrics = configuration.GetValue<bool>("GraphQL:EnableMetrics");
             if (x.EnableMetrics)
             {
               x.Schema.FieldMiddleware.Use(new InstrumentFieldsMiddleware());
             }
             x.ValidationRules = DocumentValidator.CoreRules.Concat(new[] { NoIntrospection.Instance });
             return x.EnableMetrics;
           })
		   });

I have one more question. Is there any replacement for AddRelayGraphTypes() or it is handled as a part of AddGraphTypes() ? Please confirm.

You should not need to add InstrumentFieldsMiddleware as it is already done by UseApolloTracing.

Yes, the AddGraphQL call will register the relay types automatically, regardless of whether AddGraphTypes is called.

As for NoIntrospection.Instance you probably want this instead:

.AddValidationRule(NoIntrospection.Instance)

So your AddGraphQL call might look like this:

services.AddGraphQL(b => b
    .AddSystemTextJson()
    .AddSchema<MySchema>()
    .AddGraphTypes()
    .AddValidationRule(NoIntrospection.Instance)
    .UseApolloTracing(configuration.GetValue<bool>("GraphQL:EnableMetrics"))
    .AddAuthorization(...) // for the GraphQL.Authorization nuget package
);

Using AddValidationRule can be important because your code overwrites any previously added validation rules, making it necessary to run that code prior to AddAuthorization or else the authorization rule would get dropped.

Last comment: since your configuration setting is known when your application starts, this code provides a further optimization so that the InstrumentFieldsMiddleware is not loaded in production:

services.AddGraphQL(b => {
    b.AddSystemTextJson();
    b.AddSchema<MySchema>();
    b.AddGraphTypes();
    b.AddValidationRule(NoIntrospection.Instance);
    if (configuration.GetValue<bool>("GraphQL:EnableMetrics"))
    {
        b.UseApolloTracing();
    }
    b.AddAuthorization(...); // for the GraphQL.Authorization nuget package
});

This would perform most similarly to your prior code.

@Shane32 thanks a lot for your help.

I think I just have one last question related to below code and it is related GraphQL.Authorization library. IProvideClaimsPrincipal interface is not available in latest GraphQL.Authorization library. Please help me on this.

public class GraphQLUserContext : IProvideClaimsPrincipal
  {
    /// <summary>
    ///   Gets the current users claims principal.
    /// </summary>
    public ClaimsPrincipal User { get; set; }
  }

This is pulled from IResolveFieldContext.User now, which is pulled from ExecutionOptions.User. The server library automatically sets this value to HttpContext.User. If you wish differently, you could do this:

services.AddGraphQL(b => b
    .ConfigureExecutionOptions(options => {
        options.User = /* your code here */;
    })
);

Keep in mind that services, such as IHttpContextAccessor, can be pulled from DI within that method via options.RequestServices. The delegate can be synchronous or asynchronous. And finally, note that the transport-level authorization capabilities within the server repo will not recognize such a change; please see the readme file if you plan to use those options.

If you want to retain your existing code structure, you can either (a) implement IConfigureExecution instead, or (b) copy in the old IProvideClaimsPrincipal definition and write an IConfigureExecution implementation that pulls IProvideClaimsPrincipal from DI and applies the User to the execution options. Let me know if you have any questions on this.

I would like to note that the authorization rule included with the server repo (AddAuthorizationRule builder method) supports a number of new features over the GraphQL.Authorization nuget package, such as:

  • Supports role-based authorization as well as policy-based authorization
  • Supports 'must be authenticated' rule
  • Supports 'allow anonymous' for specified fields (so that requests for only the specified field within a type are allowed when it would otherwise require authorization)
  • Correctly handles following of fragments
  • Faster with less memory requirements
  • Better support
  • Easy OOP design if you want to change how the rules/policies are checked
  • But does not support authorization rules set on input object graph types

If you choose to use the new authorization rule, you may remove the GraphQL.Authorization nuget package from your project.

Sure @Shane32 , I will try out these options and get back to you if I face any issues. Thanks for all your help.