.NET Core API 带有 GraphQL 和动态字典

.NET Core API with GraphQL and dynamic Dictionaries

我有一个 .NET Core API 应用程序,它允许用户创建 具有特定架构的模型(很像普通的 Headless CMS)。 这些模型充当内容的模板。

在模型之上创建的内容存储在 NoSQL 数据库中,拥有该内容的 class 是这样的:

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

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

使用此结构,我可以通过 API 以 JSON 格式在系统中配置的所有类型的内容提供服务,因此我可以拥有例如 Product表示的内容是这样的:

{
"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"
            }
        },
    ],
}}

或者可以是这样的Post

{
  "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"
  }
}

等等。

每种类型的模型都存储在 NoSQL DB 上自己的集合中,当我查询它们时,我的存储库接受模型类型以了解从哪个集合获取内容。

到目前为止一切都很好,我可以通过 API 提供这些内容。

现在,我的客户要求我添加一个仅用于查询文档的 GraphQL 层(因此没有突变也没有订阅)。

我尝试检查 HotChocolate 和 graphql-dotnet,但我没有找到任何让它工作的方法。 看起来它们非常适合处理在 class 中定义了所有属性的所有场景,但这里我有:

你有没有遇到过类似的情况要处理,万一怎么可能解决?

谢谢

可以动态创建模式。这实际上取决于您希望如何将数据呈现给 GraphQL API 的用户。 GraphQL 是静态类型的,因此通常您会创建带有字段的对象来表示数据。

Federation 对象中有一个 _Any 标量类型,如果您想走那条路,您可以使用它来直接支持字典,详情可在此处找到:

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

尽管如果您想创建一个包含可查询的单个对象和字段的动态模式,这里是一个示例控制台应用程序,它是一种方法。

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; }
}

示例输出:

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"
      }
    ]
  }
}

这里还有一些OrchardCMS用来动态构建字段的示例代码。

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;
}

希望对您有所帮助!