Write me a GraphQL server in C #

  • Tutorial

Somehow I had a couple of days off, and I sketched a GraphQL server to our Docsvision platform. Below I will tell you how it all went.


Poster - by magic


What a platform Docsvision


The Docsvision platform includes many different tools for building workflow systems, but its key component is something like ORM. There is a metadata editor in which you can describe the structure of card fields. There may be structural, collectible and tree sections, which, moreover, can be nested, in general, everything is difficult . A metadata generates a database, and then you can work with it through some C # API. In a word - an ideal variant for building GraphQL server.


What are the options


Honestly, there are not many options and they are so-so. I managed to find only two libraries:



UPD: in the comments suggested that there is still Hotchocolate .


According to README, at first I liked the second one, and I even started to do something with it. But soon I found out that she had too poor an API, and she could not cope with the task of generating a schema using metadata. However, it seems to have already been abandoned (the last commit a year ago).


The graphql-dotnetAPI is quite flexible, but at the same time it is badly documented, confused and unintuitive. To understand how to work with it, I had to watch the source ... True, I worked with the version 0.16, whereas now the last one 0.17.3, and 7 beta versions have already been released 2.0. So I apologize if the material is a bit old.


Must still be mentioned, libraries come with unsigned builds. I had to re-compile them from source manually to use in our ASP.NET application with signed assemblies.


GraphQL server structure


If you are not familiar with GraphQL, you can try github explorer . A little secret - you can press Ctrl + space to get auto-completion. The client part there is nothing more than GraphiQL , which can be easily screwed to your server. Just take index.html , add scripts from the npm-package, and change the url in the graphQLFetcher function to the address of your server - everything can be played.


Consider a simple query:


query { 
  viewer { 
    login,
    company
  }
}

We see here a set of fields - viewer, in it login, company. Our task, as GraphQL backend, is to build on the server a certain “scheme” in which all these fields will be processed. In essence, we just need to create an appropriate structure of service objects with a description of the fields, and set the callback function to calculate the values.


The scheme can be generated automatically on the basis of C # classes , but we will go through hardcore - we will do everything with our hands. But this is not because I am a dashing guy, just generating a schema based on metadata is a non-standard graphql-dotnet script that is not supported by official documentation. So, we dig a little in her gut, in an undocumented area.


Having created the scheme, we will remain in any convenient way for us to deliver the query string (and parameters) from the client to the server (absolutely no matter how - GET, POST, SignalR, TCP ...), and feed it to the engine along with the scheme. The engine will spit out an object with the result, which we turn into JSON and return to the client. I looked like this:


// Мой сервис, в котором генерируется схема на основе метаданныхvar schema = GraphQlService.GetCardsSchema(sessionContext);
    // Создаем экземпляр движка (объект можно переиспользовать)var executer = new DocumentExecuter();
    // Скармливаем ему схему, запросvar dict = await executer.ExecuteAsync(schema, sessionContext, request.Query, request.MethodName).ConfigureAwait(false);
    // По-простецки обработаем ошибки :)if (dict.Errors != null && dict.Errors.Count > 0)
    {
        thrownew InvalidOperationException(dict.Errors.First().Message);
    }
    // Возвращаем клиенту результатreturn Json(dict.Data);

You can pay attention to sessionContext. This is our Docsvision-specific object through which the platform is accessed. When creating a scheme, we work all the time with a particular context, but more on that later.


Schema generation


It all starts touchingly simple:


Schema schema = newSchema();

Unfortunately, this simple code ends. In order to add a field to the scheme, we need:


  1. To describe its type — create an ObjectGraphType, StringGraphType, BooleanGraphType, IdGraphType, IntGraphType, DateGraphType, or FloatGraphType object.
  2. Describe the field itself (name, handler) - create a GraphQL.Types.FieldType object

Let's try to describe that simple query that I quoted above. In the request, we have one field - the viewer. To add it to the query, you must first describe its type. His type is simple - an object, with two string fields - login and company. We describe the login field:


var loginField = new GraphQL.Types.FieldType();
loginField.Name = "login";
loginField.ResolvedType = new StringGraphType();
loginField.Type = typeof(string);
loginField.Resolver = new MyViewerLoginResolver();
// ...classMyViewerLoginResolver : GraphQL.Resolvers.IFieldResolver{
    public object Resolve(ResolveFieldContext context)
    {
        // Предполагаем, что у нас в контексте будет какой-то наш объект UserInfo// который нам передаст родительский обработчик viewerreturn (context.Source as UserInfo).AccountName;
    }
}

Similarly, we create a companyField object — well, we are ready to describe the type of the viewer field.


ObjectGraphType<UserInfo> viewerType = new ObjectGraphType<UserInfo>();
viewerType.Name = "Viewer";
viewerType.AddField(loginField);
viewerType.AddField(companyField);

There is a type, now you can describe the viewer field itself:


var viewerField = new GraphQL.Types.FieldType();
viewerField.Name = "viewer";
viewerField.ResolvedType = viewerType;
viewerField.Type = typeof(UserInfo);
viewerField.Resolver = new MyViewerResolver();
// ...classMyViewerResolver : GraphQL.Resolvers.IFieldResolver{
    public object Resolve(ResolveFieldContext context)
    {
        // Помните мы передавали свой sessionContext при выполнении запроса?// То, что мы вернем здесь будет передано дочерним резолверам (login и company)return (context.Source as SessionContext).UserInfo;
    }
}

Well, the final touch, we add our field to the query type:


var queryType = new ObjectGraphType();
queryType.AddField(viewerField);
schema.Query = queryType;

That's all, our scheme is ready.


Collections, Paging, Parameter Processing


If the field returns not one object, but a collection, then you need to explicitly indicate this. To do this, simply wrap the property type in an instance of the ListGraphType class. Suppose if the viewer returned a collection, we would simply write this:


// Было (один объект)
viewerField.ResolvedType = viewerType;
// Стало (коллекция)
viewerField.ResolvedType = new ListGraphType(viewerType);

Accordingly, in the MyViewerResolver resolver, then it would be necessary to return the list.


When collection fields appear, it is important to immediately take care of the paging. There is no ready-made mechanism here, everything is done through parameters . An example of using the parameter you might have noticed in the example above (cardDocument has an id parameter). Let's add this parameter to the viewer:


var idArgument = new QueryArgument(typeof(IdGraphType));
idArgument.Name = "id";
idArgument.ResolvedType = new IdGraphType();
idArgument.DefaultValue = Guid.Empty;
viewerField.Arguments = new QueryArguments(idArgument);

Then you can get the value of the parameter in the resolver as follows:


publicobjectResolve(ResolveFieldContext context)
{
    var idArgStr = context.Arguments?["id"].ToString() ?? Guid.Empty.ToString();
    var idArg = Guid.Parse(idArgStr);

GraphQL is so typed that Guid couldn’t parse himself. Oh well, it's not difficult for us.


Docsvision Card Request


In the implementation of GrapqhQL for the Docsvision platform, I, respectively, simply pass code through the metadata ( sessionContext.Session.CardManager.CardTypes), and for all the cards and their sections I automatically create such objects with the corresponding resolvers. The result was something like this:


query {
    cardDocument(id: "{AF652E55-7BCF-E711-8308-54A05079B7BF}") {
        mainInfo {
          name
          instanceID
        }
    }
}

Here cardDocument is the card type, mainInfo is the section name in it, name and instanceID are fields in the section. The corresponding resolvers for the card, section, and field use the CardManager API as follows:


classCardDataResolver : GraphQL.Resolvers.IFieldResolver
    {
        publicobject Resolve(ResolveFieldContext context)
        {
            var sessionContext = (context.Source as SessionContext);
            var idArg = Guid.Parse(context.Arguments?["id"].ToString() ?? Guid.Empty.ToString());
            return sessionContext.Session.CardManager.GetCardData(idArg);
        }
    }
    classSectionResolver : GraphQL.Resolvers.IFieldResolver
    {            
        CardSection section;
        public SectionFieldResolver(CardSection section)
        {
            this.section = section;
        }
        publicobject Resolve(ResolveFieldContext context)
        {
            var idArg = Guid.Parse(context.Arguments?["id"].ToString() ?? Guid.Empty.ToString());
            var skipArg = (int?)context.Arguments?["skip"] ?? 0;
            var takeArg = (int?)context.Arguments?["take"] ?? 15;
            var sectionData = (context.Source as CardData).Sections[section.Id];
            return idArg == Guid.Empty ?
                sectionData.GetAllRows().Skip(skipArg).Take(takeArg)
                :
                new List<RowData> { sectionData.GetRow(idArg) };
        }
    }
    classRowFieldResolver : GraphQL.Resolvers.IFieldResolver
    {
        Field field;
        public RowFieldResolver(Field field)
        {
            this.field = field;
        }
        publicobject Resolve(ResolveFieldContext context)
        {
            return (context.Source as RowData)[field.Alias];
        }
    }

Of course, here you can only request cards by id, but it is not difficult to generate a scheme in the same way for accessing advanced reports, services and anything. With such an API, you can get any data from the Docsvision database by simply writing the appropriate JavaScript — very convenient for writing your own scripts and extensions.


Conclusion


With GrapqhQL in .NET, it's still not easy. There is one somewhat lively library, without a reliable vendor and with an incomprehensible future, an unstable and strange API, an uncertainty about how it will behave under load and how stable it is. But we have what we have, it seems to work, and the flaws in the documentation and the rest are compensated by the openness of the source code.


What I described in this article is an increasingly undocumented API, which I researched by typing and studying the source code. It was just that the authors of the library did not think that someone would need to generate a circuit automatically — well, what can you do, this is an open source.


All this was written over several weekends, and of course, so far no more than a prototype. In the standard delivery of Docsvision this is likely to appear, but when it is hard to say. However, if you like the idea of ​​accessing the Docsvision database directly from JavaScrpit without writing server extensions - write. The higher the interest from partners - the more attention we pay to this.


Also popular now: