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 ring API to rule them all

GQL для фронта.

  • Построение запросов и получение данных самостоятельно
  • Чтение документации + схема данных
  • Упрощение общения с разработчиками АПИ

GQL для бека

  • На каждый день:
    • Описание сущностей и структур данных
    • Их связей
    • Реализация резолверов (простых функций)
  • Решение задач:
    • N+1
    • Оптимизации и унификация запросов к БД
    • Кеширование
    • Разграничение доступа

Точно, что НЕ надо делать:

  • Никаких документаций, оторванных от кода
  • и никаких /v1 /v2 /v3

Что происходит на сервере с gql-запросом

  1. Парсинг запроса
  2. Верификация запроса!
  3. Выполнение резолверов
  4. Верификация ответа!
  5. Сериализация и отправка ответа

< ответственность разработчика

  • Описание схемы

по 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# - ни о чём, начиная работу с этих статей можно легко зайти в тупик

  • Хорошая абстракция, не протекающая серверными недрами наружу, независимо от реализации самого сервера

Спасибо!

GraphQL + C#

By lawrentiy

GraphQL + C#

  • 1,169