Compress me

Strawberry Shake removes the complexity of state management and lets you interact with local and remote data through GraphQL.

На деле выгодно отличается от всего остального тем что:

  • валидирует запросы - читай не возможно очепятаться в запросе
  • использует кодо генерацию для результатов - читай не возможно забыть что в запросе убрали поле, а из модельки забыли
  • генерит интерфейсы для всего на свете - читай проще при тестировании

Демо проект:

mkdir demo
cd demo
dotnet new web
dotnet add package StrawberryShake.Transport.Http
dotnet add package StrawberryShake.CodeGeneration.CSharp.Analyzers

Единожды:

dotnet tool install StrawberryShake.Tools --global
dotnet graphql init https://dracula.rabota.ua/graphql -n DraculaClient

Эта команда стянет схему и создаст конфиг .graphqlrc.json в котором есть смысл выключить emitGeneratedCode (что бы не создавалась папка Generated, все сгенерированные файлы все равно будут доступны, плюс что бы не сводить с ума IDE), а так же по желанию включить генерацию record за вместо class для inputs и entities.

screenshot

Теперь где угодно в проекте создаем graphql файл с каким либо запросом, например:

GetPublishedVacancies.graphql

query GetPublishedVacancies($keywords: String!) {
  publishedVacancies(
    filter: { keywords: $keywords }
    pagination: { count: 3 }
  ) {
    items {
      id
      title
    }
  }
}

Примечания:

  • vscode и rider имеют graphql плагины, благодаря чему не только смогут подсветить синтаксис и подсказать что писать дальше, но и запустить запрос прямо из редактора
  • благодаря валидации, если я очепятаюсь где либо и например напишу tittle то проект просто напросто не собереться с ошибкой GetPublishedVacancies.graphql(6, 8): [SS0002] The field `tittle` does not exist on the type `Vacancy`.

Собираем проект dotnet build (первый раз, возможно понадобиться перезапустить редактор, что бы последний расчехлился и “увидел” сгенерированный код)

Дальше регистрируем наш клиент:

builder.Services
    .AddDraculaClient()
    .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://dracula.rabota.ua"));

Ну и по месту инжектим IDraculaClient и вызываем сгенерированный GetPublishedVacancies, напр:

var client = app.Services.GetRequiredService<IDraculaClient>();
var vacancies = await client.GetPublishedVacancies.ExecuteAsync("PHP");
if (vacancies.Data != null && vacancies.Data?.PublishedVacancies.Items != null)
{
    foreach (var vacancy in vacancies.Data.PublishedVacancies.Items)
    {
        Console.WriteLine($"{vacancy?.Id}: {vacancy?.Title}");
    }
}

Примечание: блин, у нас тут на выходе [Vacancy] из-за чего все все nullable 🤷‍♂️

или:

app.MapGet("/vacancies", ([FromServices] IDraculaClient client) => client.GetPublishedVacancies.ExecuteAsync("PHP"));

screenshot

Profit


Вопросы

Как обновить схему

dotnet graphql update

Нужно ли коммитать схему

Да, без нее проект не собереться

Тут кстати тоже любопытно, получается в процессе сборки благодаря этом можно дополнительно проверять, все еще актуальная схема или нет в проектче (читай обеспечивать совместимость)

Плагин rider хочет создать второй конфиг

Действительно, в rider используется “старый” подход с .graphqlconfig но для нас это совсем не проблема, так как он смотрит на тот же файл схемы

Как прокинуть токен пользователя

Что то типа такого (псевдо):

builder.Services
    .AddHttpContextAccessor()
    .AddDraculaClient()
    .ConfigureHttpClient((provider, client) =>
    {
        client.BaseAddress = new Uri("https://dracula.rabota.ua");
        client.Timeout = TimeSpan.FromSeconds(5);
        // var ctx = provider.GetRequiredService<IHttpContextAccessor>();
        // client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ctx.HttpContext?.Request.Headers.Authorization)
        // client.DefaultRequestHeaders.AcceptLanguage = ctx.HttpContext.Request.Headers.AcceptLanguage;
    });

Подробнее расписанно в доке, конфигурация вызывается на каждый запрос

Как прокинуть токен пользователя из консольного консьюмера

На примере Accep Language, но с Authorization будет работать так же

Предположим у нас есть следующий запрос:

query GetCity($id: ID!) {
  city(id: $id) {
    name
  }
}

И пример приложения которое утилизирует фичу языка AsyncLocal

using demo;
using StrawberryShake;

var builder = WebApplication.CreateBuilder(args);

builder.Services
     .AddDraculaClient()
     .ConfigureHttpClient((serviceProvider, client) =>
     {
         client.BaseAddress = new Uri("https://dracula.rabota.ua");
         if (LangOperation.Value != null)
         {
             client.DefaultRequestHeaders.Add("Accept-Language", LangOperation.Value);
         }
     });

var app = builder.Build();

var client = app.Services.GetRequiredService<IDraculaClient>();

var defaultBehavior = await client.GetCity.ExecuteAsync("1");
Console.WriteLine(defaultBehavior.Data.City.Name); // Киев

var acceptedLanguage = await LangOperation.Run("en", token => client.GetCity.ExecuteAsync("1", token));
Console.WriteLine(acceptedLanguage.Data.City.Name); // Kiev

var t1 = Task.Run(async () =>
{
    var client = app.Services.GetRequiredService<IDraculaClient>();
    var result = await LangOperation.Run("en", token => client.GetCity.ExecuteAsync("1", token));
    Console.WriteLine("t1(en): " + result.Data.City.Name); // t2(uk): Kiev
});

var t2 = Task.Run(async () =>
{
    var client = app.Services.GetRequiredService<IDraculaClient>();
    var result = await LangOperation.Run("uk", token => client.GetCity.ExecuteAsync("1", token));
    Console.WriteLine("t2(uk): " + result.Data.City.Name); // t2(uk): Київ
});

Task.WaitAll(t1, t2);

public static class LangOperation
{
    private static readonly AsyncLocal<string> Store = new();
    public static string? Value => Store.Value;
    public static async Task<IOperationResult<TResultData>> Run<TResultData>(string language, Func<CancellationToken, Task<IOperationResult<TResultData>>> implementation) where TResultData : class
    {
        Store.Value = language;
        return await implementation(CancellationToken.None);
    }
}

Проверяем поведение по умолчанию, затем эту штуку с передачей заголовка и затем в нескольких потоках.

Все это дело было найденно вот тут


С Аней столкнулись со следующей проблемой, конретно в Windows и в VisualStudio, как то ну очень странно себя все это дело ведет, если генерируемый код не виден и ничего не помогает - есть смысл включить emit генерируемых файлов, что бы они прям в папке появились