gqlgen and dependency injection using golang

882 Views Asked by At

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 !

1

There are 1 best solutions below

4
blackgreen On

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).