В статье рассказывается как транслировать запросы GraphQL в OData и выполнять их написав
совсем немного кода на C#.
Как это работает
Основная идея проекта — трансляция запросов GraphQL в OData, трансляция запросов OData в дерево выражений, которое затем транслируется в запрос для ORM. Разбор запроса GraphQL и сериализация его результатов осуществляется с помощью GraphQL for .NET. Разбор запроса OData осуществляется с помощью OData .NET Libraries. Трансляция запросов (GraphQL -> OData -> expression tree) и их выполнение осуществляется с помощью OdataToEntity.
Непосредственный доступ к данным осуществляется через ORM выполняющее полученное дерево выражений (expression tree). Выполнение запросов на различных ORM осуществляется через абстрактный класс OeDataAdapter и его реализации для:
- Entity Framework OeEf6DataAdapter
- Entity Framework Core OeEfCoreDataAdapter
- Linq2Db OeLinq2DbDataAdapter
От пользователя требуется лишь иметь контекст доступа к данным (EF/EF Core — DbContext, Linq2Db — DataConnection).
Более подробно о выполнении запросов OData можно прочитать в моей предыдущей статье OdataToEntity легкий способ создания .Net Core OData сервисов.
Пример использования
Для примера будет использоваться схема Star Wars, ORM EF Core, провайдер SQLite in memory.
Сначала надо создать контекст доступа к данным StarWarsContext. Затем адаптер доступа к данным StarWarsDataAdapter. После можно начать выполнять запрос:
String query = @"
{
human(id: ""1"") {
name
friends {
name
appearsIn {
name
}
}
}
}
";
//create data adapter
var dataAdapter = new StarWars.StarWarsDataAdapter(false, "test");
//build odata model
IEdmModel edmModel = dataAdapter.BuildEdmModelFromEfCoreModel();
//create graphql query parser
var parser = new OeGraphqlParser(edmModel);
//get graphql result
ExecutionResult result = await parser.Execute(query);
//serialize json
String json = new DocumentWriter(true).Write(result);
Console.WriteLine(json);
Запрос GraphQL:
{
human(id: ""1"") {
name
friends {
name
appearsIn {
name
}
}
}
}
Транслируется в OData:
Human?$filter=Id eq '1'&$select=Name&$expand=Friends($select=Name;$expand=AppearsIn($select=Name))
Транслируется в SQL:
SELECT
"h"."Name" AS "Item1",
"h"."Id" AS "Item2",
CASE
WHEN "t"."Id" IS NULL
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END,
"t"."Name" AS "Item10",
"t"."Id" AS "Item20",
CASE
WHEN "EpisodeEnum"."Value" IS NULL
THEN CAST(1 AS BIT) ELSE CAST(0 AS BIT)
END,
"EpisodeEnum"."Name" AS "Item11",
"EpisodeEnum"."Value" AS "Item21"
FROM "Hero" AS "h"
LEFT JOIN "HeroToHero" AS "CharacterToCharacter" ON "h"."Id" = "CharacterToCharacter"."CharacterId"
LEFT JOIN (
SELECT "Hero".*
FROM "Hero" AS "Hero"
WHERE "Hero"."CharacterType" IN (1, 2)
) AS "t" ON "CharacterToCharacter"."FriendId" = "t"."Id"
LEFT JOIN "HeroToEpisode" AS "CharacterToEpisode" ON "t"."Id" = "CharacterToEpisode"."CharacterId"
LEFT JOIN "Episodes" AS "EpisodeEnum" ON "CharacterToEpisode"."EpisodeId" = "EpisodeEnum"."Value"
WHERE ("h"."CharacterType" = 1) AND ("h"."Id" = @__Item1_0)
Результат JSON:
{
"data": {
"human": [
{
"name": "Luke",
"friends": [
{
"name": "R2-D2",
"appearsIn": [
{
"name": "NEWHOPE"
},
{
"name": "EMPIRE"
},
{
"name": "JEDI"
}
]
},
{
"name": "C-3PO",
"appearsIn": [
{
"name": "NEWHOPE"
},
{
"name": "EMPIRE"
},
{
"name": "JEDI"
}
]
}
]
}
]
}
}
Генерируемый SQL не имеет проблемы N+1 запросов, все данные получаются в одном запросе.
Структура исходного кода
Исходный код разделен на две части: в папке source — сама библиотека и сборки доступа к различным источникам данных, в папке test — тесты и примеры кода.
Сама библиотека находится в папке source/OdataEntity.GraphQL.
Тесты test/OdataToEntity.Test.GraphQL.
Файл солюшена sln/OdataToEntity.Test.GraphQL.sln.