Как стать автором
Обновить

Комментарии 28

Интересно было бы почитать как именно реализовывалось, наглядная демонстрация работы с Expression-ами.

Спасибо за комментарий. Если интересно покопаться в исходниках, вот исходный код Visitor'а, который строит Where-выражение. После рефакторинга получился совсем худенький - https://github.com/RDavydenko/SmartGraphQLClient/blob/master/src/SmartGraphQLClient.Core/Visitors/WhereExpressionVisitor/WhereExpressionVisitor.cs

Постараюсь в ближайшее время написать про работу с Expression'ами в этом проекте. Ничего сильно сложного здесь нет, но есть неочевидные подводные камни, с которыми пришлось столкнуться

Труд, конечно, объемный, но, то, что вы реализовали это вовсе не LINQ. Потому что LINQ работает в принципе не так. В нем вы не повторяете API System.Linq.Queryable, а реализуете только свой System.Linq.IQueryProvider который транслирует уже готовый expression в запрос к источнику данных провайдером для которого он является. А построением самого этого expression занимается уже готовый класс-расширение System.Linq.Queryable, который от конкретного провайдера не зависит. Статью все равно плюсанул :)

Согласен. Немного кликбейтный получился заголовок. Да, я не задавался целью реализовать свой собственный QueryProvider, хотел просто сделать похожий на Linq api, чтобы было удобно и привычно пользоваться таким GraphQL-клиентом. Под капотом здесь своя реализация некоторого IQueryable (IGraphQLQueryable), ничего общего с обычным Linq он не имеет

А почему так? Через QueryProvider же по камушкам все.

Потому что далеко не все доступные методы-расширения Linq подходят для GraphQL-запроса. Мы, например, не можем выполнить какие-нибудь Average, Sum, Aggregate, AsNoTracking и т.д. GraphQL-сервер от ChilliCream без расширений на стороне сервера поддерживает только фильтрацию, сортировку, проекцию, пагинацию и FirstOrDefault, поэтому проще сделать свой интерфейс по типу IQueryable только для GraphQL, чтобы однозначно определить методы, которые точно будет поддерживаться. Иначе пришлось бы из большей половины Linq-методов кидать исключения, что они не поддерживаются

Только AsNoTracking как раз не является стандартным, он вообще не в классе Queryable находится. И как раз его-то при желании можно и поддержать (например, безопасно проигнорировать).


А вот с остальным согласен.

Это штатная для LINQ ситуация. Точно так же не каждый LINQ-запрос может быть оттранслирован в SQL. В этом случае провайдер просто кидает exception.

Вообще-то, LINQ — это встроенный в C# синтаксис запросов, тот который from … select …. И ему не важно какой так тип данных для построения запроса используется.

Это все все равно транслируется во fluent API из System.Linq.Queryable (который, по факту, все и используют - ни разу не видел чтобы кто-то реально использовал from ... select синтаксис).

И ему не важно какой так тип данных для построения запроса используется.

Ровно до тех пор, пока этот тип данных реализует IQueryable.

ни разу не видел чтобы кто-то реально использовал from … select синтаксис

Ну вот я его часто использую, особенно когда надоедает парные скобки выискивать...


Ровно до тех пор, пока этот тип данных реализует IQueryable.

Хотите сказать, что его нельзя применить ни к IEnumerable, ни к EntityQuery из WCF RIA Services? Советую попробовать и убедиться что всё прекрасно работает.

что его нельзя применить ни к IEnumerable

Можно. Только он опять-таки оттранслируется в расширения, только уже из System.Linq.Enumerable и, соответственно, работать будет несколько по-другому. Про WCF RIA, вот, реально не знаю, потому что видел его только раз и то краем глаза.

Я не говорю о том как оно будет работать. Я говорю о том, что синтаксис linq транслируется в вызовы методов независимо от того где эти методы объявлены, и это работает для любого класса. Главное — чтобы методы были видимы.

from ..select отлично подходит, когда надо использовать join

НЛО прилетело и опубликовало эту надпись здесь

По идее было бы лучше избежать текстовых значений. Банальная опечатка проблемы создаст.

Вынесите в класс с константами и проблем не будет.

Если схема генерируется на базе SDL

Схема не генерируется. Не добавлял такой функциональности. В принципе, и так полно инструментов, которые генерируют классы из GraphQL-схемы.

Сделать типизированный класс-Endpoint - хорошая идея, мне нравится. Только я бы через конструктор тогда уж сохранял строку, а не через атрибут. В проекте есть возможность добавить атрибут [GraphQLEndpoint("users")] на сущность, но надо понимать, что одна и та же сущность может использоваться разных методах с разными endpoint'ами, поэтому лучше либо передавать строки, либо сделать такой типизированный класс-endpoint.

Определить интерфейсы и сделать что-то типа

Include<IRoles>() .ThenInclude<IUsers>()

или даже

Include<IRoles, IUsers>()

Не вижу смысла, т.к. это уже не похоже на Linq. Цель была - сделать удобный для всех инструмент взаимодействия с GraphQL-сервером. Чтобы все, кто умеет работать с Linq на примере того же EntityFramework, без проблем поняли, что и зачем тут происходит.

Часто вместе идут, можно сахара добавить SkipAndTake(5, 10)

Первый раз такое вижу, но если кому-то сильно надо, то можно же написать свой собственный метод-расширения.

Можно ещё добавить собственный анализатор, чтобы в compile time ловить не поддерживаемые expressions. Иначе определённый тип ошибок только в runtime будет вылезать.

В compile time - это, конечно, круто

Вынесите в класс с константами и проблем не будет.

Ещё как будут. Смотрите:


client.Query<CarModel>(Constants.Users)

Вроде и в константу вынесено что нужно, да вот что-то работать не будет… Надо не класс с константами делать, а аналог DbContext, для начала — хотя бы вот такой:


class QueryContext {
    private readonly IGraphQLClient client;

    public QueryContext(IGraphQLClient client) {
        this.client = client;
    }

    public Query<UserModel> Users => client.Query<UserModel>("users");
}

Ну от ошибок и опечаток никто не застрахован. Идея с QueryContext мне нравится 👍

НЛО прилетело и опубликовало эту надпись здесь

Понял теперь о чем речь. Ну я думаю, что большинство функционала, который не является стандартным в том же Linq, и здесь можно оставить на откуп пользователя. Кому понадобится что-то необычное - добавит свой extension себе локально. В комментариях уже были примеры со SkipAndTake , в принципе, и это можно реализовать, если кому-то понадобится

НЛО прилетело и опубликовало эту надпись здесь

Ну и самое главное, что если в схеме что-то изменится, при перегенерации QueryContext эти изменения сломают компиляцию, что очень сильно поможет в избежании проблем.

Да, я задумывался о том, чтобы не возиться со строками, чтобы при компиляции ловить ошибки, т.к. клиент все-таки типизированный. Но еще можно сделать инфраструктурный Nuget-пакет с выходными моделями, и в нужном сервисе подключать его. Обновится Nuget-пакет - если изменились модели, будет ошибка на этапе компиляции

Но для этого, конечно, нужно следить за актуальностью версии Nuget-пакета в проекте

А можно вопрос - как реализуются в GraphQL пермишены. Типо не все поля или не все типы можно возвращать для каких-то пользователей.

Permission'ы - т.е. доступ на основе Permission'ов нужно делать на стороне GraphQL-сервера. Думаю, в сети можно найти примеры, погуглив что-то типа "PermissionBased Authorization HotChocolate".

Чтобы возвращать не все поля - тут посложнее будет, т.к. GraphQL-схема штука статичная и должна быть заранее известна при обращении к ней. Самый простой метод - это создать DTO и предоставить пользователю метод, возвращающий, например, список таких DTO. Тогда у пользователя не будет доступа до полей, которые ему не нужно видеть.

Можно и в рантайме вычищать ответ от GraphQL-сервера, ставля в null, например, какие-то поля, до которых у пользователя нет доступа, но, если честно, мне это не нравится. Нужно будет тогда еще и в фильтрацию, сортировку залазить, чтобы по недоступным полям выкидывать из запроса фильтрацию/сортировку. В общем, те еще костыли с велосипедами

Добавлю еще один вариант. Можно на стороне сервера возвращать заранее отфильтрованную/отсортированную IQueryable<>, например. Например:

public IQueryable<Entity> GetMyEntities(
  [Service] IUnitOfWork unitOfWork, 
  [Service] IUserAccessor userAccessor)
{
    var userId = userAccessor.GetCurrentUserId();
    return unitOfWork.Entities.Where(x => x.CreateUserId == userId);
}

Класс, то что нужно чтобы в graphql ходить из .net. Пара вопросов:

  • чем-то можно классы из gql-схемы сгенерить? Ну, чтобы не руками писать.

  • что с перформансом? Получается как-нибудь строки с запросами кэшировать, или хотя-бы прям в utf8 сразу писать?

1) При помощи этой библиотеки - нет, но должна быть точно не одна библиотека для генерации классов из схемы.
Можно посмотреть в сторону StrawberryShake - https://chillicream.com/docs/strawberryshake/v13/tooling

2) Пока кэширования не предусмотрено, надо подумать в эту сторону. Кстати, хорошая идея, чтобы добавить возможность запрос и при помощи строк для большей вариативности, хоть я и хотел от этого уйти, но лишним не будет, если появится

По производительности могу добавить, что ничего сверхъестественного здесь нет - для строк используется StringBuilder, для запросов в graphql-серверу - обычный HttpClient, а для десериализации ответа - System.Text.Json

По поводу 1 - хорошо бы, если было бы решение вместе с генерацией. Не обязательно ее делать в рамках самой библиотеки, но хорошо бы проверить что с каким-то сторонним генератором это работает.

Т.е. полноценное решение я вижу как-то так:

  • качаете GQL схему (в виде результата introspection query) в файл, кладете его в репу

  • запускаете команду из пакета X, у вас получается набор cs-файлов под эту схему

  • подключаете твою библиотеку

  • можете ходить в нужный GQL-API, с автокомплитом в IDE, строго-типизированно

По поводу 2 - может быть про перформанс - это чисто страхи, и на деле плодить строчки на каждый запрос - не так накладно. Может быть, если померить, там и не страшно. Но, как идеи для оптимизации:

  • уметь писать запрос сразу в сокет (минуя построение большой UTF-16 строки в памяти, и конвертацию ее дальше в UTF-8 - как теперь делает тот же встроенный JSON-сериализатор)

  • сделать некие pre-build queries - возможность написать (параметризированный) запрос, вызвать ему .Build(), и положить в какую-нибудь static-переменную.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории