GraphQL + .NET
Павел Шалаев
Рецепты, инструменты, личный опыт
Как я дожил до доклада такого?
- с 2010: WinForms > ASP.NET > ASP MVC > Core > много всякого повидал
- с 2015: Meteor > Apollo > GraphQL полюбил
- 2017 год: доклад "GraphQL - с чего начать"
Что я не буду делать... постараюсь
- Сравнивать gql и Rest
- Сравнивать gql и SQL
- Сравнивать gql и gRpc
- Сравнивать gql и oData
- Сравнивать gql и Swagger
- Отвечать, как решаются стандартные задачи, появляющиеся с приходом graphQL
- Говорить, что это серебряная пуля
А афтер-пати будет???
Берегу ваше время
Эти разговоры могут затянуться... ;)
Сегодня четверг...?
menti.com
47 56 30
Кратко повторим
- gql - это договор общения клиент-сервер
- Получение данных - запросы
- Изменение данных - мутации
- Строгие типы, схема данных
- Что запросил, то и получил
- На сервере - ресолверы для реакции на запросы
- one
ringAPI to rule them all
GQL для фронта.
- Построение запросов и получение данных самостоятельно
- Чтение документации + схема данных
- Упрощение общения с разработчиками АПИ
GQL для бека
- На каждый день:
- Описание сущностей и структур данных
- Их связей
- Реализация резолверов (простых функций)
- Решение задач:
- N+1
- Оптимизации и унификация запросов к БД
- Кеширование
- Разграничение доступа
Точно, что НЕ надо делать:
- Никаких документаций, оторванных от кода
- и никаких /v1 /v2 /v3
Что происходит на сервере с gql-запросом
- Парсинг запроса
- Верификация запроса!
- Выполнение резолверов
- Верификация ответа!
- Сериализация и отправка ответа
< ответственность разработчика
- Описание схемы
по Githubу поскреби, по Nugetу помети....
- Обязательно
- По желанию
GraphQL.NET
Базовый
Работает
Много ***
с октября 2015
-
Слабо развивается
Документация на 3-
Hot Chocolate
Отличное название!
Много фич
Быстро развивается
Есть истории переезда
Документация на 4+
с июня 2018
-
много фич
не пробовал по-полной
странное название...
HotChocolate дополнительные няшеньки
-
Azure Function or AWS Lambda.
-
Schema Stitching
-
Query batching
-
Persisted Queries
-
Any Type
-
Attributes
-
Relay Support
-
Filter and Sorting Support
-
Reach Event and Middlewares (compare to GQL.NET)
-
Apollo Tracing
Playgroundы
- GraphiQL (простой, понятный)
- Altair (на 5+)
- GraphiQL-explorer (форк от графикл)
- voyager (визуализация схем)
- Banana Cake Pop (сырой)
- Graphql.Playground (на 5, есть apollo-tracing)
- Почти бесплатно
- биндинг схемы gql на данные через json-файл
- возможность внедрения в схему работы в ресолверах
Это еще не страшно и не больно, поверьте ;)
type Author {
id: ID!
name: String
"""
the list of books by this author
"""
books: [Book]
}
enum BookType {
FICTION
NON_FICTION
}
type Book {
id: ID!
title: String
author: Author
votes: Int
type: BookType
}
type Query {
authors: [Author]
books: [Book]
}
type Mutation {
upvoteBook (
bookId: ID
): Book
}
Примеры описания схемы
public class AuthorType : ObjectGraphType
{
public AuthorType()
{
Name = "Author";
Description = "Автор";
Field<NonNullGraphType<IdGraphType>>("id");
Field<NonNullGraphType<StringGraphType>>("name", "Имя автора");
Field<ListGraphType<Book>>("books", "Книги автора",
new QueryArguments()
{
new QueryArgument(typeof(IntGraphType))
{
Name = "limit",
DefaultValue = 10,
Description = "Все не выводим. А вдруг, Донцова!"
}
},
ResolveBooks // Должен возвращать массив книг
);
}
}
Как описать схему gql (на примере GraphQL.NET)
Query.AddField(new FieldType {
Name = "allAuthors",
Description = "Список авторов с фильтрацией и пагинацией",
Type = typeof(ListGraphType<AuthorsType>),
Arguments = new QueryArguments(
new QueryArgument<PaggingType> {Name = "paging"},
new QueryArgument<AuthorsFilterType> {Name = "filter"}
),
Resolver = new FuncFieldResolver<ListGraphType<AuthorType>,
IEnumerable<Author>>(context =>
{
var pagging = context.GetArgument<Pagging>("paging");
var filter = context.GetArgument<AuthorFilterType>("filter");
var subFields = context.SubFields;
var userCtx = (UserContext)context.UserContext;
// Тут может быть LINQ, http-запрос, обращение к любой базе и что угодно
var queryBuilder = generateQuery(...)
.AddPagination(pagging);
var q = queryBuilder.Build();
var res = sqlManager.DoQuery(...);
return res;
})
}
Как описать схему gql (на примере GraphQL.NET)
еще поломаем глаза....
[GraphQLObject]
public class Author : IDisposable
{
[GraphQLField(Name="id", ReturnType=typeof(NonNullGraphType(IdGraphType))]
public Guid Id { get; set; }
[GraphQLField()]
public string Name { get; set; }
[GraphQLFunc]
public IEnumerable<Book> books(ResolveFieldContext context)
{
return new List<Book>() {new Book {}};
}
public void Dispose()
{
Db.Dispose();
}
}
С помощью аннотаций (GraphQL.NET.Annotations)
var schema = SchemaBuilder.New()
.AddDocumentFromString(
@"
type Query {
hello: String
}")
.AddResolver("Query", "Hello", () => "world")
.Create();
С помощью схемы (Hot Chocolate)
{
allAuthors {
name
books {
title
author {
name
books {
title
author {
name
}
}
}
}
}
}
DataLoader - решаем N+1 как facebook завещал
//Startup.cs
services.AddDataLoaderRegistry();
// Резолвер
descriptor
.Field("author")
.Type<AuthorType>().Resolver(ctx =>
{
IDataLoader<Guid, Author> dataLoader =
ctx.BatchDataLoader<Guid, Author>("AuthorById", GetAuthorsAsync);
return dataLoader.LoadAsync(ctx.Parent<Book>().authorId);
});
// Получение данных
private async Task<IReadOnlyDictionary<Guid, Author>> GetAuthorsAsync(
IReadOnlyCollection<Guid> ids,
CancellationToken cancellationToken)
{
var res = _repository.DB.Connection
.SoftBuild()
.From("authors")
.WhereIn("id",ids)
.List<Author>();
return res.ToDictionary(t => t.id);
}
DataLoader - решаем N+1 как facebook завещал
addQueryList("allAuthors", "Список всех авторов", (db, filter) =>
{
// Динамическое формирование запроса
if (filter.ids != null && filter.ids.Length > 0)
{
return (from a in db.Authors
join fi in db.AuthorsFullInfo on a.Id equals fi.AuthorId
where filter.ids.Contains(a.Id)
where a.DDate == null
// Как сделать список полей?
select new {a.Id, a.Name, a.Borndate, fi.publisherName}
);
}
return (from a in db.Authors
join fi in db.AuthorsFullInfo on a.Id equals fi.AuthorId
join b in db.Books on b.AuthorId = a.Id
where a.DDate == null
&& (
filter.q != null && (
u.name.Contains(filter.q)
||fi.publisherName.Contains(filter.q)
||b.title.Contains(filter.q)
)
|| filter.q == null
)
select new {a.Id, a.Name, a.Borndate, fi.publisherName}
);
});
GraphQL.NET + Entity Framework + MS SQL
Проблемы
- сложная фильтрация
- выборка полей
- объединение запросов
public static SqlQueryBuilder generateQuery(SqlManager sqlManager, ResolveFieldContext context)
{
var q = new SqlQueryBuilder("authors", "a");
q.AddFields(new [] {"id"}); // обязательные поля
if (context.SubFields != null)
{
foreach (KeyValuePair<string,Field> fl in context.SubFields)
{
switch (fl.Key)
{
case "organisation":
q.AddFields(new [] {"a.orgId", "a.orgName", "a.orgINN"});
break;
case "totalBooks": // Всего книг
q.AddFields(new []
{$"totalBooks = (SELECT COUNT(b.Id) FROM dbo.books b WHERE b.authorId=a.Id))"});
break;
case "historyInfo": // История редактирования автора (добавляются дополнительные поля)
HistoryInfoType.addQueries(q, "calc", fl.Value);
default:
q.AddField(fl.Key);
break;
}
}
}
... следующий слайд
}
GraphQL.NET + генерация схем + MS SQL
Добавляем условия фильтрации
public static SqlQueryBuilder generateQuery(SqlManager sqlManager, ResolveFieldContext context)
{
... Предыдущий слайд
var filter = context.GetArgument<AuthorFilterType>("filter");
var f = new List<FilterOperation>();
if (filter.ids != null && filter.ids.Any())
f.Add(new FilterOperation("a.id", PropertyType.text, OperationType.In, filter.ids));
if (filter.name != null)
f.Add(new FilterOperation("a.name", PropertyType.string, OperationType.Like, filter.name));
if (f.Count > 0) q.AddFilter(f);
return q;
}
GraphQL.NET + генерация схем + MS SQL
Добавляем условия фильтрации
Это удалось реализовать и работает быстро
Обработка ошибок
- Стандарт
- Удобно отлаживать
- Стабильность
{
"errors": [
{
"message": "Unexpected Execution Error",
"locations": [ { "line": 7, "column": 9 } ],
"path": ["allAuthors", 0, "books", 0, "author", "name"],
"extensions": {
"message": "Could not cast the source object to `tmp.Models.Author`.",
"stackTrace": " at HotChocolate.Execution.ResolverContext.Parent[T]()\r\n at lambda_method(Closure , IResolverContext )\r\n at HotChocolate.Types.FieldMiddlewareCompiler.<>c__DisplayClass3_0.<<CreateResolverMiddleware>b__0>d.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at HotChocolate.Execution.ExecutionStrategyBase.ExecuteMiddlewareAsync(ResolverContext resolverContext, IErrorHandler errorHandler)"
}
}
],
"data": {
"allAuthors": [
{
"name": "Кинг",
"books": [
{
"title": "book name 111",
"author": {
"name": null,
"books": null
}
}
]
}
]
}
}
Как войти в мир gql
- Как источник документации (лайвхак, очень просто)
- Как новые endpoint`ы (а клиенты готовы?)
- Упорядочить внутренности (туда-сюда конвертация)
- Чистый заход (а клиенты готовы, а программисты?)
Мои выводы
-
Разобраться стоит каждому разработчику
-
Сравнивая реализации в JS и на .NET: много возможных ходов в .NET, сложно сразу понять, как начинать, как изучать, как сделать правильно
-
Инструменты достаточно развиты
-
Большинство статей gql+c# - ни о чём, начиная работу с этих статей можно легко зайти в тупик
-
Хорошая абстракция, не протекающая серверными недрами наружу, независимо от реализации самого сервера
Спасибо!
-
Очень полезно
-
Просто полезно
-
Однотипные обучалки
-
Дополнительные пакеты (генераторы схем, клиенты на .NET и т.д.)
-
Пример проблем с EF
-
Вредно
GraphQL + C#
By lawrentiy
GraphQL + C#
- 1,155