1

im using gqlgen lib to implement a graph ql server .. the following setup code is straigthforward

    port := os.Getenv("PORT")
    if port == "" {
        port = defaultPort
    }

    graphCfg := graph.Config{
        Resolvers: graph.NewRootResolver(),

        // TODO: add complexity calc and shield
        //Complexity: graph.ComplexityRoot{},
        //Directives: graph.DirectiveRoot{},
    }
    graphSchema := graph.NewExecutableSchema(graphCfg)

    var gqlSrv http.Handler
    gqlSrv = handler.NewDefaultServer(graphSchema)
    gqlSrv = factory.Middleware(gqlSrv)
    http.Handle("/graphql", gqlSrv)

Root Resolver:

package graph

import (
    communicationResolvers "github.com/venn-city/service-venn-graph/modules/communication/resolvers"
    realEstateResolvers "github.com/venn-city/service-venn-graph/modules/realestate/resolvers"
    socialResolvers "github.com/venn-city/service-venn-graph/modules/social/resolvers"
)

// Generate gql models
//go:generate go run github.com/99designs/gqlgen generate

// This file will not be regenerated automatically.
// It serves as dependency injection for your app, add any dependencies you require here.

type RootResolver struct{}

func NewRootResolver() *RootResolver {
    return &RootResolver{}
}

as the comment states // It serves as dependency injection for your app, add any dependencies you require here.

the issue is that RootResolver is created once for the entire application lifetime .. it would have been much simpler if i had the ability to create a scoped IOC container per request ..

My current approach is to write a custom middleware / handler that will create the executable schema per-request so that RootResolver will be create as new per request …

as an example I would like to create a logger with a RequetId field and pas that logger to all resolvers and lower levels of logic to use same logger.

would very much appreciate any insights on that ! thanks !

Mortalus
  • 10,574
  • 11
  • 67
  • 117

1 Answers1

1

Recreating the root resolver at each request isn't a good idea. The dependency injection the code comment talks about is service-scoped dependencies. Stuff that you need to set into the root resolver once. For example configuration values, caches, etc.

If you need request-scoped dependencies, use gqlgen middlewares with the Around* methods available on the Server type. These middleware functions get a context.Context as first argument, you can set request-scoped values in there.

The gqlgen source contains a code comment block that explains how the request lifecycle works (author: Brandon Sprague):

  //  +--- REQUEST   POST /graphql --------------------------------------------+
  //  | +- OPERATION query OpName { viewer { name } } -----------------------+ |
  //  | |  RESPONSE  { "data": { "viewer": { "name": "bob" } } }             | |
  //  | +- OPERATION subscription OpName2 { chat { message } } --------------+ |
  //  | |  RESPONSE  { "data": { "chat": { "message": "hello" } } }          | |
  //  | |  RESPONSE  { "data": { "chat": { "message": "byee" } } }           | |
  //  | +--------------------------------------------------------------------+ |
  //  +------------------------------------------------------------------------+
  • AroundOperations is called before each query, mutation or subscription operation.
  • AroundResponses is called before returning the response
  • AroundRootFields is called before entering each root resolver. In the query example above, it's called before the viewer resolver.
  • AroundFields is called before each field resolver.

In case of a logger, you might use AroundOperations. You can compute whatever value you need in the middleware itself, or use values initialized in the enclosing scope.

logger := // create some logger

gqlSrv = handler.NewDefaultServer(graphSchema)

gqlSrv.AroundOperations(func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
        ctx = context.WithValue(ctx, "request-id-key", "1234")
        ctx = context.WithValue(ctx, "logger-key", logger)
        // call next to execute the next middleware or resolver in the chain
        next(ctx)
    })

Then in your resolver implementations, you can access those values from the context as usual: ctx.Value("logger-key"). (Note that instead of strings, it's better to use unexported structs as context keys).

blackgreen
  • 34,072
  • 23
  • 111
  • 129
  • accessing values from context result in dependency obfuscation, i would very much like to have a method paramater based DI system .. and yes logger s a dependency.. can you elaborate on why re-creating a root resolver is bad ? – Mortalus May 14 '23 at 10:31
  • because you need a root resolver to instantiate the server. *Maybe* (big maybe) you could get this to work with some amount of hackery, but I don't know if there's any chance of corrupting the server's internal state. Anyway, this would pretty clearly stray from the intended usage, when you have better ways to achieve what you want. Context injection *is* the preferred Go idiom for request-scoped dependencies. – blackgreen May 15 '23 at 01:11
  • do you have any ref fro that "Context injection is the preferred Go idiom for request-scoped", from what i heard and from the gophers slack seems like its very discouraged and i can see the drawbacks as well. maybe there is something in the middle .. – Mortalus May 15 '23 at 12:23
  • 1
    It is a commonly understood best practice. I don’t have handy links right now. What comes to mind is the documentation of the `context` package and perhaps the official Go FAQs. Anyway, do what works best for you. I gave you the advice I was comfortable giving you :) – blackgreen May 15 '23 at 12:40