.NET Core API with GraphQL and dynamic Dictionaries

1.5k views Asked by At

I have a .NET Core API app that allow users to create models with specific schema (much like common Headless CMS). The models act as Template for contents.

The content created on top of models are stored in NoSQL Database and the class that owns the content is something like this:

public class Content
{
    public string Id { get; set; }

    public IDictionary<string, object> Data { get; set; }
}

with this structure I am able to serve via API all types of contents configured in the system in a JSON format, so I can have for example a Product content that is represented like this:

{
"id": "625ea48672b93f0d68a9c886",
"data": {
    "code": "ABC",
    "has-serial-number": true,
    "in-catalogue": true,
    "in-customer-care": true,
    "name": "Product Name",
    "main-image": {
        "id": "32691756a12ac10a5983f845",
        "code": "fjgq7ur3OGo",
        "type": "image",
        "originalFileName": "myimage.png",
        "format": "png",
        "mimeType": "image/png",
        "size": 4983921,
        "url": "https://domain/imageurl.xyz",
        "height": 1125,
        "width": 2436,
        "metadata": {
            "it-IT": {
                "title": "Image Title"
            }
        }
    },
    "main-gallery": [{
            "id": "62691756a57cc10a5983f845",
            "code": "fjgq7ur3OGo",
            "type": "image",
            "originalFileName": "myimage.png",
            "format": "png",
            "mimeType": "image/png",
            "size": 4983921,
            "url": "https://domain/imageurl.xyz",
            "height": 1125,
            "width": 2436,
            "metadata": {
                "it-IT": {
                    "title": "Image Title"
                }
            }
        }
    ],
    "thumbnail-video": null,
    "subtitle": "Product subtitle",
    "description": "Product description",
    "brochure": true,
    "category": [{
            "id": "525964hfwhh0af373ef6",
            "data": {
                "name": "Category Name",
                "appMenuIcon": {
                    "id": "62691756a57cc10a5983f845",
                    "code": "fjgq7ur3OGo",
                    "type": "image",
                    "originalFileName": "mycategoryimage.png",
                    "format": "png",
                    "mimeType": "image/png",
                    "size": 4983921,
                    "url": "https://domain/imageurl.xyz",
                    "height": 1125,
                    "width": 2436,
                    "metadata": {
                        "it-IT": {
                            "title": "Image title"
                        }
                    }
                },
                "subtitle": "Category subtitle",
                "media": "6258058f632b390a189402df",
                "content": "Category description"
            }
        },
    ],
}}

or can be a Post like this

{
  "id": "6270f5f63934a209c0f0f9a2",
  "data": {
    "title": "Post title",
    "date": "2022-04-28T00:00:00+00:00",
    "type": "News",
    "content": "Post content",
    "author": "Author name",
    "tags": ["tag1","tag2","tag3"],
    "media": {
      "id": "6270f5f03934a209c0f0f9a1",
      "code": "ZvFuBP4Ism9",
      "type": "image",
      "originalFileName": "image.jpg",
      "format": "jpg",
      "mimeType": "image/jpeg",
      "size": 329571,
      "url": "https://domain/imageurl.xyz",
      "height": 1609,
      "width": 2560,
      "metadata": {
        "it-IT": {
          "title": "image title"
        }
      }
    },
    "linkWebSite": "link to web"
  }
}

and so on.

Each type of model has is storeod in its own collection on the NoSQL DB and when I query them, my repository accept the model type to know from which collection get the content.

Everything is fine so far and I can serve this contents via API.

Now, my customer asked me to add a GraphQL layer only for querying documents (so no mutations and not subscriptions).

I tried to check HotChocolate and graphql-dotnet, but I didn't find any way to let it work. Looks like that they are perfect to handle all scenario where there are defined all the properties in the class, but here I have:

  • Many Types, but only 1 class that represents them
  • The class has a Dictionary to store data, so no clr properties created upfront
  • Those library seems to work only at startup time, but I can create new model at runtime

Have you ever had a similar scenario to handle and in case how is it possible to solve?

Thank you

1

There are 1 answers

1
Joe McBride On BEST ANSWER

It is possible to create a schema dynamically. It really depends on how you want to present the data to the user of the GraphQL API. GraphQL is statically typed, so typically you would create Objects with Fields to represent the data.

There is an _Any scalar type in the Federation objects which you can use to support dictionaries directly if you want to go that route, details found here:

https://github.com/graphql-dotnet/graphql-dotnet/issues/669#issuecomment-674273598

Though if you want to create a dynamic schema with individual Objects and Fields that can be queried, here is a sample console app that is one approach.

using GraphQL;
using GraphQL.Types;
using GraphQL.SystemTextJson;

var postContent = new Content
{
    Id = Guid.NewGuid().ToString("D"),
    Data = new Dictionary<string, object>
    {
        { "title", "Post Title 1" },
        { "content", "Post content 2" },
        { "date", DateTime.Today }
    }
};

// creating graph types based on the content of the object
var postType = new ObjectGraphType { Name = "Post" };
postType.Field("id", new IdGraphType());
foreach (var pair in postContent.Data)
{
    // this attempts to find a maching GraphQL IGraphType based on the
    // .NET type of pair.Value
    // see all of the type mappings here:
    // https://graphql-dotnet.github.io/docs/getting-started/schema-types

    // you could also have your own mapping, which you will probably
    // need to for nested types in the dictionary - ex:
    // string -> StringGraphType
    // DateTime -> DateGraphType
    // etc.
    var clrGraphType = pair.Value.GetType().GetGraphTypeFromType(isNullable: true);

    // this is assuming the given types have no constructor arguments - use
    // your preferred DI container to resolve complex types
    IGraphType graphType = (IGraphType)Activator.CreateInstance(clrGraphType);
    Console.WriteLine($"GraphType for {pair.Key} is {clrGraphType.Name}");
    postType.Field(
        pair.Key,
        graphType,
        resolve: fieldContext =>
    {
        // get the original Content object
        var content = (Content)fieldContext.Source;

        // lookup the value of the field based on the field name
        // since that is what was used to create the type
        return content.Data[fieldContext.FieldDefinition.Name];
    });
}

var queryType = new ObjectGraphType();
queryType.Field(
    "posts",
    new ListGraphType(postType),
    resolve: context =>
    {
        // lookup posts in the DB
        var posts = new List<Content>
        {
            postContent,
            new Content
            {
                Id = Guid.NewGuid().ToString("D"),
                Data = new Dictionary<string, object>
                {
                    { "title", "Post Title 2" },
                    { "content", "Post content 2" },
                    { "date", DateTime.Today.AddDays(-1) }
                }
            }
        };

        return posts;
    });

ISchema schema = new Schema { Query = queryType };
schema.RegisterType(postType);

var json = await schema.ExecuteAsync(_ =>
{
    _.Schema = schema;
    _.Query = "{ posts { id title content date } }";

    // can use this to debug schema setup
    _.ThrowOnUnhandledException = true;
});

Console.WriteLine(json);

class Content
{
    public string Id { get; set; }
    public IDictionary<string, object> Data { get; set; }
}

Example output:

DynamicGraphQL% dotnet run

GraphType for title is StringGraphType
GraphType for content is StringGraphType
GraphType for date is DateTimeGraphType
{
  "data": {
    "posts": [
      {
        "id": "19d5bf3ab65c4632968597aa444eef40",
        "title": "Post Title 1",
        "content": "Post content 2",
        "date": "2022-05-11T00:00:00-07:00"
      },
      {
        "id": "29bb6ee9a86b42a1af1caa7ccbe4a6cd",
        "title": "Post Title 2",
        "content": "Post content 2",
        "date": "2022-05-10T00:00:00-07:00"
      }
    ]
  }
}

Here is also some sample code that OrchardCMS uses to dynamically build fields.

https://github.com/OrchardCMS/OrchardCore/blob/526816bbf6fc597c4910e560468b680ef14119ef/src/OrchardCore/OrchardCore.ContentManagement.GraphQL/Queries/ContentItemQuery.cs

public Task BuildAsync(ISchema schema)
{
    var field = new FieldType
    {
        Name = "ContentItem",
        Description = S["Content items are instances of content types, just like objects are instances of classes."],
        Type = typeof(ContentItemInterface),
        Arguments = new QueryArguments(
            new QueryArgument<NonNullGraphType<StringGraphType>>
            {
                Name = "contentItemId",
                Description = S["Content item id"]
            }
        ),
        Resolver = new AsyncFieldResolver<ContentItem>(ResolveAsync)
    };

    schema.Query.AddField(field);

    return Task.CompletedTask;
}

Hope that helps!