query={}Nr: 3 W poprzednich dwóch wpisach stworzyliśmy proste API w GraphQL. Widzieliśmy jak odpytywać nasze API specyficznym JSON-em w GraphQL Playground. Jak to jednak wygląda w prawdziwej aplikacji? Jak wygląda taki gołe polecenie HTTP POST czy HTTP GET?

Czym jest dokładnie zapytanie w GraphQL. W kontekście pisania aplikacji GraphQL zapytanie można tworzyć w definicji Schema. Ono dziedziczy po ObjectGraphType.

Dla klienta "zapytanie" to specyficznym zapis polecenia w formie JSON? Dzisiaj się na tym skupimy.  O ile jestem fanem rozwiązania Swagger , który generuje formatkę do testowania REST API. To w tym przypadku dla GraphQL potrzebujemy oddzielnego programu lub aplikacji aby GraphQL przetestować. Gdy zrozumiemy jak się odpytuje GraphQL API poleceniami POST i GET, wtedy napiszemy swoją stronę w ASP.NET Core, która odczyta nasze napisane w poprzednich wpisach API.

Użyjmy do testu darmowej aplikacji Postman.

Po utworzeniu konta oraz zainstalowania programu Postman musiałem jeszcze zmienić to ustawienie "SSL certificate verification".

SSL certificate verification Postman

Jak więc wygląda nasze zapytanie GET to naszego API GraphQL. W adresie określam parametr query, a wnim piszę JSON z informacją o tym, co chcemy pobrać.

Oto pełny URL : https://localhost:5001/graphql?query={dragons{id}}

Postman Query GET do GraphQL

Jak widzisz zapytaliśmy nasze API o wszystkie smoki z bazy i chcieliśmy pobrać tylko ich "id".

W poważnej aplikacji GraphQL zapytanie będzie bardzo złożone, a ilość znaków w adresie URL jest ograniczona. Poza tym chyba nie chcesz pokazywać światu czego szukasz w danym API.

Zobaczmy jak zrobić dokładnie to samo tylko używając polecenia HTTP POST.

Najpierw do nagłówka polecenia HTTP dodaje informację o tym, w jakim formacie wyśle informację.

Content-Type : application/json

Content-Type : application/json

W zakładce BODY napisałem następujący JSON :

{
    "query": "{dragons {id} }"
}

Jak widać na załączonym obrazku polecenie HTTP Post wykonało się poprawnie.

 HTTP Post w Postman w GraphQL

Zapytania w JSON wyglądają podobnie do zapytań pisanych w GraphQL Playground. Różnica polega na tym, że całe zapytanie najpierw musi otoczyć w wyrażenie "query".

Gdy piszesz swoją pierwszą aplikację GraphQL warto najpierw przetestować jakie "query" możesz napisać w GraphQL Playground. Masz tam Intellisense.  Dopiero potem możesz się bawić w wysłanie całego JSON przez normalnego klienta.

W obu odpowiedziach jak widzisz nasze smoki z bazy SQLite zostały otoczone dodatkowym polem tekstowym "data". Warto o tym pamiętać przy deserializacji JSON na obiekty po stronie klienta.

Na koniec możesz zobaczyć jak wygląda gołe zapytanie dla pojedynczego smoka o ID 1.

 HTTP Post dla jednego Smoka

Aliasy

Czy można zmienić nazwy pól i obiektów zwracanych? Czy smok zawsze będzie zwracany jako "dragon"? Czy imię zawsze będzie zwracane jako "name"?

Oczywiście możesz to zmienić poprzez zastosowanie aliasów. Powiedzmy, że chcemy zmienić "dragon" na "results". Robisz to w taki sposób.

{
    "query": "{ result:dragon(id:1) {name} }"
}

Oto dowód, że to zadziałał na prawdziwej aplikacji.

Przykład użycia Aliasó w Postman

Ta sama sztuczka działa dla pól.

{
    "query": "{result:dragon(id:1) {name,story:description} }"
}

Oto jak zmieniłem pole description na "story". Jak widzisz klient może dla swojego własnego ułatwienia nazwać pola jak mu się to podoba. 

Pora ci pokazać inny potężny funkcjonalność GraphQL. Normalnie w REST API nie możesz uzyskać dostępu do kilku źródeł informacji w jednym poleceniu HTTP.  W GraphQL możesz zapytać się o wiele rzeczy naraz. Oto przykład.

{
    "query": "{dragon(id:2) { name } dragons {id}}"
}

Oto rezultat tego zbiorowego zapytania. Dodałem drugiego smoka do bazy, abyśmy mieli inne wyniki niż wcześniej.

{
    "data": {
        "dragon": {
            "name": "Doman"
        },
        "dragons": [
            {
                "id": 1
            },
            {
                "id": 2
            }
        ]
    },
    "extensions": {
        "tracing": {
            "version": 1,
            "startTime": "2021-06-04T17:31:29.378759Z",
            "endTime": "2021-06-04T17:31:31.997759Z",
            "duration": 2619314800,
            "parsing": {
                "startOffset": 2069900,
                "duration": 24499
            },
            "validation": {
                "startOffset": 2095100,
                "duration": 57199
            },
            "execution": {
                "resolvers": []
            }
        }
    }
}

A co się stanie, jeśli zrobię takie zbiorowe zapytanie ?

{
    "query": "{dragon(id:2) { name } dragon(id:1) { name }}"
}

Jak widzisz tutaj występuje jakiś problem ? Bo mamy tylko jeden wynik, a powinniśmy mieć dwa:

"data": {
    "dragon": {
        "name": "Doman"
    }
},

GraphQL nie łączy wyników podobnych zapytań. Zamiast tego wyświetla tylko pierwszy wynik zapytania dla konkretnego aliasu. Oznacza to, że jeśli chcemy pobrać drugiego smoka to musimy przypisać mu jakiś alias.

{
    "query": "{ dragon(id:2) { name } d2:dragon(id:1) { name }}"
}

Oto wynik :

"data": {
    "dragon": {
        "name": "Doman"
    },
    "d2": {
        "name": "Franko"
    }
},

Fragmenty

Super, że możemy wykonywać zapytania  kilka zapytań jednym poleceniem HTTP.  Jednak czy istnieje jakiś sposób, aby pisać mniej powtarzającej się składni JSON.

A dokładniej listy pól.

Co, jeśli chcesz zwrócić 10 konkretnych smoków?  Byłoby wspaniale, gdyby powtarzająca się część zapytania mogła być napisana raz w jakiś centralny sposób, tak aby inne zapytania mogły z niej skorzystać.

Na pomoc przychodzą fragmenty.

Nasz JSON stanie się zaraz bardziej złożony dlatego w Postman wybieram teraz opcję "GraphQL", aby nie musiał się martwić, że mój JSON musi być pisany bez znaku nowej linii.

Fragmenty w GraphQL w Postman

Oto użycie fragmentów. Jak widzisz zadeklarowałem coś co wygląd jak funkcja w JavaScript.  We fragmencie mówię, na jakim typie chce operować, a potem podaje pola do wyświetlenia.

query 
{
     d1:dragon(id:2) { ...myfields } 
     d2:dragon(id:1) { ...myfields }
}

fragment myfields on DragonType {
    id, name, price,introducedAt,color,breath
}

Oto wynik takiego zapytania :

{
    "data": {
        "d1": {
            "id": 2,
            "name": "Doman",
            "price": 399.5,
            "introducedAt": "2020-04-11T00:00:00+01:00",
            "color": "GOLD",
            "breath": "ICE"
        },
        "d2": {
            "id": 1,
            "name": "Franko",
            "price": 219.5,
            "introducedAt": "2021-04-28T09:13:26.4835538+01:00",
            "color": "RED",
            "breath": "NONE"
        }
    },
    "extensions": {
        "tracing": {
            "version": 1,
            "startTime": "2021-06-04T17:48:04.2158898Z",
            "endTime": "2021-06-04T17:48:06.4968898Z",
            "duration": 2280772599,
            "parsing": {
                "startOffset": 1735700,
                "duration": 76000
            },
            "validation": {
                "startOffset": 1814200,
                "duration": 203100
            },
            "execution": {
                "resolvers": []
            }
        }
    }
}

Nazwanie zapytań i zmienne

GraphQL pozwala także tobie nazywać twoje zapytania.

query twodragons
{
 d1:dragons(id:2) { name } 
 d2:dragon(id:1) { name}
}

Jak widzisz nazwa zapytania nie wpływa na rezultat.  Tylko po co je nazywać?

{
    "data": {
        "d1": {
            "id": 2,
            "name": "Doman",
            "price": 399.5,
            "introducedAt": "2020-04-11T00:00:00+01:00",
            "color": "GOLD",
            "breath": "ICE"
        },
        "d2": {
            "id": 1,
            "name": "Franko",
            "price": 219.5,
            "introducedAt": "2021-04-28T09:13:26.4835538+01:00",
            "color": "RED",
            "breath": "NONE"
        }
    },
    "extensions": {
        "tracing": {
            "version": 1,
            "startTime": "2021-06-04T17:53:43.8914284Z",
            "endTime": "2021-06-04T17:53:46.5104284Z",
            "duration": 2618759900,
            "parsing": {
                "startOffset": 4168899,
                "duration": 38100
            },
            "validation": {
                "startOffset": 4207300,
                "duration": 113599
            },
            "execution": {
                "resolvers": []
            }
        }
    }
}

Mając nazwy możesz teraz łatwiej posegregować wiele zapytań do API. Istnieje jednak pewne różnica w takim podziale zapytań. Gdy wykonasz takie zapytanie to zobaczysz, że GraphQL wykonał tylko pierwsze zapytanie z tych dwóch.

query twodragons
{
 d1:dragon(id:2) { name } 
 d2:dragon(id:1) { name }
}
query all {
    dragons { price }
}

Po co więc nazewnictwo ? Otóż w dalszej definicji zapytania możesz określić która operacja ma się wykonać tym razem. Aby to pokazać muszę przejść na tryb RAW w Postman, gdyż definicja operacji nie zawiera się w "query".

{
    "query": 
            "query twodragons { d1:dragon(id:2) { name }  d2:dragon(id:1) { name }}query all {dragons { price }} "
            }
    "operationName" : "all" 
}

Na tym jednak opcję się nie kończą. Możesz do zapytań przekazywać parametry. Tutaj zaczynają się schody i naprawdę polecam pisać najpierw polecenia w GraphQL PlayGround.

query twodragons($d1Id: ID!, $d2Id: ID!) 
{ 
  d1:dragon(id:$d1Id) { name }  
  d2:dragon(id:$d2Id) { name }
}

query all {dragons { price }} 

Zmienne w nim podajesz odrębnym oknie "query variables".

GraphQL Playground Query Variables

W GraphQL PlayGround wybierasz operację wciskając przycisk play.

W GraphQL PlayGround wybór Query

Jak to jednak wygląda w gołym zapytaniu RAW JSON w PostMan.

{
    "query": 
            "query twodragons($d1Id: ID!, $d2Id: ID!) { d1:dragon(id:$d1Id) { name }  d2:dragon(id:$d2Id) { name }}query all {dragons { price }} ",
    "operationName" : "twodragons" ,
    "variables": {  "d1Id": 2,"d2Id": 1}
}

Jeśli chodzi o definiowanie zmiennych, to warto zaznaczyć, że GraphQL ma pewne gotowe typy i są one połączone z tym, co zdefiniowaliśmy po stronie naszej aplikacji w .NET

  • ID! jest dla .NET mapowany jako IdGraphType
  • Boolean! dla .NET mapowany jako dla BooleanGraphType
  • Float! jest dla .NET mapowany jako FloatGraphType
  • Int! jest dla .NET mapowany jako IntGraphType
  • String! jest dla .NET mapowany jako StringGraphType

Nasze zapytanie "twodragons" przyjmuje dwa parametry typu ID stąd typ ID!.

Potem gdy te zmienne uzupełniamy to nie używamy już znaku dolara. 

Dyrektywy

To jeszcze nie koniec. W zapytaniu także możesz umieszczać prostą logikę, która będzie odpowiednio modyfikować wynik. Oto jak zapytanie wygląda w PlayGround GraphQL.

query all ($showMore: Boolean!)
{
  dragons { 
    id,
    name,
    price @include(if: $showMore)
    color @include(if: $showMore)
    breath @include(if: $showMore)
  }
} 

Rzeczywiście takie zapytanie działa :

Dyrektywy w GraphQL Playground przykład użycia

Dla zmiennej "showMore" jako prawda mamy więcej pól. Co więcej, możesz też określić wartość domyślną danego parametru. Tak aby GraphQL nie wariował, gdy jej nie podasz.

query all ($showMore: Boolean! = false)
{
  dragons { 
    id,
    name,
    price @include(if: $showMore)
    color @include(if: $showMore)
    breath @include(if: $showMore)
  }
} 

Oto jak zapytanie wygląda jako goły JSON wysłany przez PostMan bezpośrednio do GraphQL :

{
    "query": 
            "query twodragons($d1Id: ID!, $d2Id: ID!) { d1:dragon(id:$d1Id) { name }  d2:dragon(id:$d2Id) { name }}query all ($showMore: Boolean!){dragons { id,name,price @include(if: $showMore)color @include(if: $showMore)breath @include(if: $showMore)}}  ",
    "operationName" : "all" ,
    "variables": {  "d1Id": 2,"d2Id": 1, "showMore":true}
}

Błędy i twoje komunikaty

Napisanie złego zapytania skutkuje błędem.  GraphQL ma swoje zasady walidacyjne. Co ciebie może ciekawić ty możesz napisać swoją zasadę walidacyjną.

Wracając do naszego kodu napisanego w .NET w definicji danego zapytania możemy dodać swoją logikę walidacyjną:

public class DragonQuery : ObjectGraphType
{
    public DragonQuery(DragonRepository dragonRepository)
    {

        Field<DragonType>(
            "dragon",
            arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>>
            { Name = "id" }),
            resolve: context =>
            {
                var id = context.GetArgument<int>("id");
                if (id >= 3)
                    context.Errors.Add(new ExecutionError("Id is wrong"));

                //var user = (GraphQLUserContext)context.UserContext;
                //if (user.User.Identity.Name == "Cez") { }


                return dragonRepository.GetOne(id);
            }
        );

        Field<ListGraphType<DragonType>>(
            "dragons",
            resolve: context => dragonRepository.GetAll()

        );
    }
}

Przykładowo teraz, jeśli ID będzie równe bądź większe niż 3 to zwrócę błąd dla zapytań o konkretne smoki.

Tak operacja nie przerywa procesu wyszukiwania smoków. Błąd jednak zostanie wyświetlony:

Wyświetlenie swojego błędu w GraphQL

Klient w ASP.NET Core, który odczyta nam nasze API

Doszliśmy do tego momentu aby napisać aplikację, która odczyta nasze API GraphQL. W sumie to wiem co możemy zrobić z zapytaniem więc nie mamy już powodu, aby dłużej czekać.

Każdy język programowania i framework ma jakiś sposób wywołania polecenia HTTP. Ja to zrobię w C#.

Tworzy więc nowy projekt ASP.NET Core.

Tworzy więc nowy projekt ASP.NET Core do naszej solucji

Do naszej aplikacji, która będzie odczytywać nasze GraphAPI potrzebujemy modele, które będą zdeserializowane z JSON-a, którego otrzymamy. 

Jakie modele będziemy mieli w solucji w Visual Studio

Potrzebujemy generyczny typ na każde odpowiedzi pochodzące z GraphQL. Pamiętaj, że w tej odpowiedzi, też mogą być błędy :

public class Response<T>
{
    public T Data { get; set; }
    public List<ErrorModel> Errors { get; set; }

    public void ThrowErrors()
    {
        if (Errors != null && Errors.Any())
            throw new GraphQlException(
                $"Message: {Errors[0].Message} Code: {Errors[0].Code}");
    }
}

public class GraphQlException : ApplicationException
{
    public GraphQlException(string message) : base(message)
    {
    }

Mam model błędów:

public class ErrorModel
{
    public string Message { get; set; }
    public string Code { get; set; }
}

Mam model swojego smoka wraz z jego typami wyliczeniowymi. Pamiętaj nasze API nie przekazuje wszystkich właściwości smoka. 

public class DragonModel
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public decimal Price { get; set; }

    public int Rating { get; set; }

    public ColorTypeEnum Color { get; set; }

    public BreathTypeEnum Breath { get; set; }

    public DateTimeOffset IntroducedAt { get; set; }
}

public enum ColorTypeEnum
{
    Red,
    Yellow,
    Gold,
    Orange,
    Blue,
    Green,
    Purple,
}
public enum BreathTypeEnum
{
    None,
    Fire,
    Ice,
    Thunder,
    Acid,
}

Oto także klasa reprezentująca opinie ekspertów na temat smoków.

public class DragonExpertOpinionModel
{
    public int Id { get; set; }

    public string Title { get; set; }

    public string Review { get; set; }
}

Potrzebujemy także klasy, która będzie trzymała listę wszystkich smoków.

public class DragonsInList
{
    public List<DragonModel> Dragons { get; set; }
}

Do deserializacji JSON skorzystam z paczki NuGet Newtonsoft.Json

Newtonsoft.Json paczka NuGet

Odczytanie GraphQL API przy pomocy HTTPClient

W .NET wykonujemy zapytania przy pomocy klasy HTTPClient. Z nim są pewne problemy, jeśli go się źle użyje, o czym pisałem tutaj. W tym wpisie nie chce się na tym skupiać. Mogę Ci tylko powiedzieć, że mam dobry powód aby użyć "IHttpClientFactory".

Chce stworzyć kilka klientów HTTP na każdy typ danych, które chce pobrać. 

Oto więc HTTPClient dla smoków. Może w następnym wpisie pojawią się może kolejne klasy napisane w podobnym stylu.

public class DragonHttpClient
{
    private readonly HttpClient _httpClient;

    public DragonHttpClient(IHttpClientFactory httpClient)
    {
        _httpClient = httpClient.CreateClient("DragonHttpClient");
    }

    public async Task<Response<DragonsInList>> GetDragons()
    {
        var response = await _httpClient.GetAsync(@"?query= 
        { dragons 
            {     id,name,description,introducedAt,rating,color,breath  } 
        }");
        var stringResult = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<Response<DragonsInList>>(stringResult);
    }
}

Trzeba dodać obsługę i konfigurację naszej strony klienta ASP.NET CORE. Tutaj ustawiamy adres URL do naszego GraphQL API.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient("DragonHttpClient", client =>
        {
            client.BaseAddress = new Uri("https://localhost:5001/graphql");
            client.Timeout = new TimeSpan(0, 0, 30);
            client.DefaultRequestHeaders.Clear();
        });
        services.AddScoped<DragonHttpClient>();
        services.AddControllersWithViews();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseStaticFiles();
        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
             endpoints.MapDefaultControllerRoute();
        })
    }
}

Deklarujemy także użycie kontrolerów z widokami. Celowo ustawiam DragonHttpClient jako Scoped, który klasa będzie tworzona z każdym zapytaniem HTTP. Instancjami HTTPClient zajmie się interfejs "IHttpClientFactory ".

W kontrolerze głównym pozostaje nam skorzystać z naszego HttpClienta.

public class HomeController : Controller
{
    private readonly DragonHttpClient _httpClient;

    public HomeController(DragonHttpClient httpClient)
    {
        _httpClient = httpClient;
    }


    public async Task<IActionResult> Index()
    {
        var responseModel = await _httpClient.GetDragons();
        responseModel.ThrowErrors();
        return View(responseModel.Data.Dragons);
    }
}

Do strony w folderze wwwroot dodaje odpowiednie pliki CSS. Dodaje też odpowiednie widoki, które wyświetlą listę wszystkich naszych smoków.

Pamiętasz metoda Index z kontrolera Home będzie szukać widoku z folderu Home o nazwie Index.cshtml.

@model List<DragonShop.Website.Models.DragonModel>

<div class="row">
    <h4 class="mb-3 ml-5">Dragons that you can buy:</h4>
</div>

@foreach (var dragon in Model)
{
<div class="row">
    <div class="col-2 my-auto">@dragon.Name</div>
    <div class="col-2 my-auto">@dragon.Color</div>
    <div class="col-2 my-auto">@dragon.Breath</div>
    <div class="col-2 my-auto">$@dragon.Price</div>
    <div class="col-2 my-auto stars">
        @for (var i = 0; i < @dragon.Rating; i++)
        {
<text>*</text>}
    </div>

</div>}

Dodanie widoków do klienta aplikacji Index.cshtml

Uruchamiamy stronę i jak widzisz, jeśli wiesz jak odpytać swoje API GraphQL to zostało Ci tylko napisać już kod specyficzny dla konkretnego frameworka.

Rezultat strony Index dla Dragon Shop

Czy można odczytać API jeszcze inaczej w .NET? Bo jak pamiętasz pisanie gołych JSON-ów miejscami robi się kłopotliwe. Przydałabym nam się jakieś gotowe klasy, które nam ułatwią tworzenie zapytań.

Zanim jednak do tego przejdziemy stwórzmy podstronę, która będzie pobierać dane konkretnego smoka. Do HttpClient dodajemy następującą metodę i ze względu na złożoność zapytania wysyłamy teraz polecenie HTTP POST.

public async Task<Response<DragonContainer>> GetDragon(int id)
{
    dynamic request = new JObject();

    request.query = @"
        query dragonQuery($dragonId: ID!)
        {
            dragon(id: $dragonId)
                {
                id name description introducedAt rating color breath price
                  opinions { title review }
            }
    }";
    dynamic variables = new JObject();
    variables.dragonId = 1;
    request.variables = variables;

    var content = new StringContent(request.ToString(), Encoding.UTF8, "application/json");

    var response = await _httpClient.PostAsync("", content);
    var stringResult = await response.Content.ReadAsStringAsync();
    return JsonConvert.DeserializeObject<Response<DragonContainer>>(stringResult);
}

Aby stworzyć tak skomplikowane wyrażenie JSON skorzystałem z dynamicznej klasy JObject z paczki NuGet Newsoft.JSON.

Struktura zapytania JSON którą widać w Visual Studio

W Visual Studio warto zawsze zdebugować aplikację i zobaczyć jaki konkretny JSON jest wysłany do naszego GraphQL API.

Możesz się zastanawiać skąd wzięła się klasa DragonContainer.

public class DragonContainer
{
    public DragonModel Dragon { get; set; }
}

Jak się okazuje potrzebuje takiej klasy, gdyż struktura zwracanego JSON przez GraphQL nie daje mi na chwilę obecną dojścia bezpośrednio do swojego smoka. Patrząc na strukturę zwracanego JSON-a możesz to zauważyć.

StringResult czyli JSON rezultatu zapytania o jednego smoka

Dlaczego o tym mówię? Ponieważ przez półgodziny miałem problem z deserializacją tego obiektu. Nie trzeba dodawać atrybutów. Wystarczy po prostu utworzyć zależność klas według określonego schematu.

Pozostaje jeszcze dodać metodę w kontrolerze, która wyświetli nam widok na pojedynczego smoka.

public class HomeController : Controller
{
    private readonly DragonHttpClient _httpClient;


    public HomeController(DragonHttpClient httpClient)
    {
        _httpClient = httpClient;

    }

    public async Task<IActionResult> DragonDetailOld(int id)
    {
        var d = await _httpClient.GetDragon(id);
        return View("~/Views/Home/DragonDetail.cshtml", d.Data);
    }

    public async Task<IActionResult> Index()
    {
        var responseModel = await _httpClient.GetDragons();
        responseModel.ThrowErrors();
        return View(responseModel.Data.Dragons);
    }

}

Dodajemy także widok, który obsłuży nam pojedynczego smoka.

Dodanie nowego widoku DragonDetail.cshtml

W widoku razor rozpisuje właściwości smoka na poszczególne elementy HTML.

@model DragonShop.Website.Models.DragonContainer;

<div class="row">
    <div class="col-9">
        <div class="row">
            <div class="col-12">
                <h3>@Model.Dragon.Name</h3>
            </div>
        </div>
        <div class="row mb-3">
            <div class="col-12">
                @Model.Dragon.Description
            </div>
        </div>
        <div class="row mb-4">
            <div class="col-3">
                In store since: @Model.Dragon.IntroducedAt.Year
            </div>
            <div class="col-3">
                Stars: @Model.Dragon.Rating
            </div>
            <div class="col-3">
                Price: $@Model.Dragon.Price
            </div>
        </div>
        <h6>Reviews:</h6>
        <ul></ul>
        @foreach (var op in Model.Dragon.Opinions)
        {
<div class="row">
    <div class="col-12"><h5>@op.Title</h5></div>
</div>
                <div class="row mb-2">
                    <div class="col-12">@op.Review</div>
                </div>}
        <ul></ul>6
    </div>
</div>

Oto nasz rezultat:

strona dragondetail.cshtml

Zobaczmy jak możemy osiągnąć dokładnie to samo tylko tym razem zastosujemy gotowe rozwiązanie, jakie oferują nam paczki NuGet.

Sam będziesz mógł ocenić czy Twój kod jest czytelniejszy niż czyjś w paczce NuGet.

Odczytywanie GraphQL API przy pomocy paczki NuGet GraphQL.Client

Jak większość spraw i problemów w .NET i ta ma swoją własną paczkę NuGet?

Paczka NuGet GraphQL.Client

Oto kod użycia tej paczki NuGet. Możesz zauważyć, że dla zmiennych mamy oddzielne właściwości przy tworzeniu zapytania. Sam klient też ma już gotowe zdeserializowane pola przy odpowiedzi do danego zapytania trzeba tylko ustawić odpowiednie typy generyczne.

public class DragonGraphClientFromNuget
{
    private readonly IGraphQLClient _client;

    public DragonGraphClientFromNuget(GraphQLHttpClient client)
    {
        _client = client;
    }

    public async Task<DragonModel> GetDragon(int id)
    {
        var query = new GraphQLRequest
        {
            Query = @" 
            query productQuery($dragonId: ID!)
            { dragon(id: $dragonId) 
                { id name description introducedAt rating color breath price
                  opinions { title review }
                }
            }",
            Variables = new { dragonId = id }
        };
        var response = await _client.SendQueryAsync<Response<DragonModel>>(query);
        return response.Data.Data;
    }

    public async Task<Response<DragonsInList>> GetDragons()
    {
        var query = new GraphQLRequest
        {
            Query = @" 
            { dragons 
                {     id,name,description,introducedAt,rating,color,breath,price  } 
            }"
        };
        var response = await _client.SendQueryAsync<Response<DragonsInList>>(query);
        return response.Data;

    }

Dodajmy nową metodę w kontrolerze oraz widoki i zobaczymy czy ten kod działa.

public class HomeController : Controller
{
    private readonly DragonHttpClient _httpClient;
    private readonly DragonGraphClientFromNuget _dragonGraphClientFromNuget;

    public HomeController(DragonHttpClient httpClient,
        DragonGraphClientFromNuget dragonGraphClientFromNuget)
    {
        _httpClient = httpClient;
        _dragonGraphClientFromNuget = dragonGraphClientFromNuget;
    }

    public async Task<IActionResult> DragonDetail(int id)
    {
        var d = await _dragonGraphClientFromNuget.GetDragon(id);
        return View(d);
    }

    public async Task<IActionResult> DragonDetailOld(int id)
    {
        var d = await _httpClient.GetDragon(id);
        return View("~/Views/Home/DragonDetail.cshtml", d.Data);
    }

    public async Task<IActionResult> Index()
    {
        var responseModel = await _httpClient.GetDragons();
        responseModel.ThrowErrors();
        return View(responseModel.Data.Dragons);
    }
}

Potrzebuje dodatkowej paczki do deserializacji, gdy korzystamy z tego rozwiązania. Dodajemy więc ją. "GraphQL.Client.Serializer.Newstonsoft"

GraphQL.Client.Serializer.Newstonsoft Nuget paczka

W klasie startup konfigurujemy instancje "IGraphQLClient".  Jak widzisz można do niego dodać wygenerowanego HTTPClienta z IHttpClientFactory.

Ciągle próbuje uniknąć problemu, który opisałem w tym wpisie.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient("DragonHttpClient", client =>
        {
            client.BaseAddress = new Uri("https://localhost:5001/graphql");
            client.Timeout = new TimeSpan(0, 0, 30);
            client.DefaultRequestHeaders.Clear();
        });

        services.AddScoped<DragonHttpClient>();
        services.AddScoped<IGraphQLClient>
            ((service) =>
                {

                    var f = service.GetRequiredService<IHttpClientFactory>();
                    var opt = new GraphQLHttpClientOptions();

                    return new GraphQLHttpClient(opt,
                        new NewtonsoftJsonSerializer(), 
                        f.CreateClient("DragonHttpClient"));
                }
            );
        services.AddScoped<DragonGraphClientFromNuget>();
        services.AddControllersWithViews();
    }

I to wszystko. Osiągneliśmy ten sam efekt tylko korzystając z gotowym rozwiązań. Jak się domyślasz efekt jest dokładny taki sam.

Osobiście ta paczka NuGet nie ułatwiła tak bardzo pisania i odbierania wiadomości do naszego API GraphQL.