AddOpinionNr: 4 Poprzednich wpisach stworzyliśmy API Sklepu Smoków i ją odczytaliśmy w innej aplikacji ASP.NET CORE. Na razie jednak nasze API GraphQL ma zapytania, które odczytują dane.

Jak wiadomo API powinno nie tylko odczytywać dane, ale także je zmieniać.

Nasza Schema obecnie wygląda tak i zawiera tylko Query. Zdefiniowaliśmy wcześniej jak dane mogą być odpytywane. Na razie daliśmy możliwość pobrania wszystkich smoków oraz pobranie jednego konkretnego smoka. 

Query i Schema przykład

Co trzeba jednak zrobić aby dodać możliwość kasowania, dodawania lub zmieniania danych.

Query plus Mutation

Wszystkie polecenia, które coś modyfikują już nie są "Query". GraphQL nazywa je "Mutation" .

Spełnia ona te same zasady co "Query" po stronie API.  W kodzie po prostu definiujemy trochę inaczej pola i obiekty, które mają być przesłane do modyfikacji. 

Potrzebujesz jeszcze czegoś innego, gdy piszesz definicję "Mutation" czyli Mutacji. 

Jak działa Mutation dla GraphQL

W tym wpisie zobaczymy jak mutację działają. Dodamy do naszego GraphQL API możliwość dodania opinii na temat danego smoka.

Polecenie dodające nową opinie musi naturalnie go wysłać jakoś w formacie JSON. Nie będzie on tego samego typu co w przypadku Query i musi on dziedziczy po innym typie z frameworka GraphQL.

GraphQL robi wyraźnie rozróżnienie pomiędzy danymi, które mają być wysłane jako odpowiedz na zapytanie, czyli query, a danymi, które są wysłane do modyfikacji. Żadne dane nie mogą być użyte ponownie w innym kontekście odczytu czy zapisu.

Co jeszcze zrobimy w tym tekście? W stworzonym wcześnie kliencie ASP.NET Core wyślemy tą mutację i zobaczymy jak polecenie JSON ma dokładnie wyglądać, aby taką operację wykonać.

InputGrapType

Naszą przygodę musimy rozpocząć od stworzenia nowego typu dla mutacji, ponieważ inne wcześniej napisane typy nie mogą być użyte ponownie.

Dodanie DragonExpertOpinionInputType do GraphQL

Ten typ zawiera informację o polach, które będzie przyjmować w naszym poleceniu dodającym opinię. 

Możemy określić, że tytuł opinii nie może być pusty jak Id smoka, a poza tym określamy ich typy danych czy coś będzie stringiem, czy liczbą całkowitą int.

public class DragonExpertOpinionInputType
    : InputObjectGraphType
{
    public DragonExpertOpinionInputType()
    {
        Name = "opinionInput";
        Field<NonNullGraphType<StringGraphType>>("title");
        Field<StringGraphType>("review");
        Field<NonNullGraphType<IntGraphType>>("dragonId");
    }
}

Dodatkowo w tym przypadku musimy dziedziczyć po klasie "InputObjectGraphTy[e"

Dodanie mutacji (mutowanie) do DragonShop API

Zanim dodamy kod polecenia, który będzie dodawał naszą opinię warto najpierw dodać taki kod do repozytorium

Korzystamy z Entity Frameworka Core więc taki kod dodania nowej opinii do bazy zajmuje tylko parę linijek kodu.

public class DragonExpertOpinionRepository
{
    private readonly DragonShopDbContext _dbContext;

    public DragonExpertOpinionRepository(DragonShopDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<DragonExpertOpinion> AddExpertOpinionAsync(DragonExpertOpinion op)
    {
        _dbContext.DragonExpertOpinion.Add(op);
        await _dbContext.SaveChangesAsync();

        return op;
    

W folderze GraphQL dodajemy nową klasę, która będzie definiować wszystkie mutację na smokach.  Analogicznie DragonQuery przechowuje wszystkie zapytania, które możemy zrobić na smokach.

Dodanie DragonMutation do GraphQL

Tutaj definiujemy jak nasze polecenie:

  • Ma się nazywać
  • Jakie ma argument przyjmuje i tutaj się pojawia typ, który napisaliśmy paragraf wcześniej
  • Opisujemy także jak ono ma się wykonać i tutaj pojawia się nasza metoda z repozytorium
public class DragonMutation : ObjectGraphType
{
    public DragonMutation(DragonExpertOpinionRepository dragonExpertOpinionRepository)
    {
        Field<DragonOpinionType>(
            "createOpinion",
            arguments: new QueryArguments(
                new QueryArgument<NonNullGraphType<DragonExpertOpinionInputType>> { Name = "opinion" }
                )
              ,
              resolve:  context =>
             {
                 var op = context.GetArgument<DragonExpertOpinion>("opinion");

                 var r = dragonExpertOpinionRepository.AddExpertOpinionAsync(op).Result;
                 return op;

             });
    }
}

Może Cię zdziwić użycie  "Result" z operacji asynchronicznej. W momencie pisania tego wpisu nie wiem, dlaczego pewne metody z poprzedniej wersji GrapQL nie działają i co ciekawe nie można użyć słowa kluczowego async i await w tym kontekście. Mam nadzieje, że to poprawie w przyszłości.

Pozostało dodać te definicje mutacji do Schema.

Dodanie mutacji do Schema

Schema ma gotową właściwość na to. Nazywa się ona Mutation.

public class DragonSchema : Schema
{
    private readonly DragonShopDbContext _dbContext;

    public DragonSchema(DragonShopDbContext dbContext, IServiceProvider sp) : base(sp)
    {
        _dbContext = dbContext;

        Query = new DragonQuery
           (new DragonRepository(_dbContext));

        Mutation = new DragonMutation(
            new DragonExpertOpinionRepository(_dbContext));
    }
}

Uruchamiamy nasz serwer GraphQL i wygenerowanej dokumentacji stworzonej przez GraphQL Playground widzimy nasze nowe definicję typów, jak i poleceń.

Jakie typy mamy w GraphQL

W zakładce Docs także widzimy podział na Queries i na Mutations.

GraphQL Doc dokumentacja

W playground GraphQL możemy przetestować zapytanie do naszego API. Polecenia modyfikujące coś zaczynają się od "Mutation" zamiast "Query".

Nasza mutacja przyjmuje parametr, który jest typem naszej opinii, który został przez nas utworzony.

W tej definicji może też ustalić co powinno być zwrócone po wykonaniu tego polecenia.

GraphQL Playground składnia mutatora i zmienne

W oddzielnym oknie dodajemy zmienne, które jak widzisz są zapisem JSON naszej opinii na temat danego smoka.

Oto treść zapytania:

mutation($opinion: opinionInput!) {
  createOpinion(opinion: $opinion){
    id,title
  }
}

A to JSON zmiennych:

{
  "opinion": {
    "title": "Wonderfull",
    "review": "yeah",
    "dragonId": 2
  }
}

Jak widzimy w Playground GraphQL zapytanie wykonało się poprawnie i zostały nam zwrócone te same dane, które wysłaliśmy.

Zwrócona informacja przez Playground GraphQL

Jak to wygląda u prawdziwego klienta? Wróćmy do klienta, który został przez nas napisany w poprzednim wpisie. 

Dodanie polecenia modyfikacji do klienta ASP.NET Core

Pamiętasz odpytywaliśmy nasze GraphQL API na dwa sposoby. Raz przez gołą klasę HTTPClient, a raz przez klasy paczki NuGet "GraphQL.Client".

Zobaczmy co trzeba dodać. Oto nowa metoda "AddOpinion" do klasy DragonHttpClient.

public class DragonHttpClient
{
    private readonly HttpClient _httpClient;

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

    public async Task<Response<DragonExpertOpinionModel>> AddOpinion(DragonExpertOpinionModel op)
    {
        dynamic request = new JObject();

        request.query = @"
             mutation($opinion: opinionInput!) {
                createOpinion(opinion: $opinion){
                    id,title
                }
        }";
        dynamic opinion = new JObject();
        opinion.dragonId = op.DragonId;
        opinion.review = op.Review;
        opinion.title = op.Title;

        dynamic variables = new JObject();
        variables.opinion = opinion;
        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<DragonExpertOpinionModel>>(stringResult);
    }

Tworzenie JSON przez obiekt dynamiczny JObject z paczki NuGet "Newtonsoft.Json" może wydawać się dziwne, ale lepsze to niż operowanie na String-ach czy String.Builder-ach.

Nasze gołe zapytanie Post jako JSON do nowej metody w GraphQL wygląda tak. Na szczęście białe znaki jak spacje czy entery są ignorowane. 

struktura gołego zapytania POST do GraphQL

Zobaczmy jak podobne polecenie do GraphQL można utworzyć przez paczkę NuGet  "GraphQL.Client".

public class DragonGraphClientFromNuget
{
    private readonly IGraphQLClient _client;

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

    public async Task<DragonExpertOpinionModel> AddOpinion(DragonExpertOpinionModel opinion)
{ var request = new GraphQLRequest { Query = @" mutation($opinion: opinionInput!) { createOpinion(opinion: $opinion){ id,title } }", Variables = new { opinion }
}; var response = await _client.SendMutationAsync<DragonExpertOpinionModel>(request); return response.Data; }

Jak pamiętasz te rozwiązanie mam gotową właściwość dla zmiennych. Tutaj istnieje jednak pewna pułapka związana z deserializacja modelu  "DragonExpertOpinionModel". Jeśli jego właściwości mają inne nazwy albo istnieje w nim jakiś parametr za dużo to ten kod wykona się błędnie.

Można temu zapobiec tworząc typ anonimowy do właściwości Variables.

public async Task<DragonExpertOpinionModel> AddOpinion(DragonExpertOpinionModel op)
{
    var opinion = new
    {
        Title = op.Title,
        Review = op.Review,
        DragonId = op.DragonId
    };
    var request = new GraphQLRequest
    {
        Query = @" 
        mutation($opinion: opinionInput!) {
          createOpinion(opinion: $opinion){
            id,title
          }
        }",
        Variables = new { opinion }
    };

    var response = await _client.SendMutationAsync<DragonExpertOpinionModel>(request);
    return response.Data;
}

Do głównego kontrolera zostało nam dodać nową metodę, która załaduje formularz do dodania opinii.

Dodajemy także dwie HTTP POST, które na te dwa sposoby wyślę do GraphQL API nową opinię.

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

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

    public IActionResult AddOpinion(int dragonid)
    {
        return View(new DragonExpertOpinionModel()
        { DragonId = dragonid });
    }

    [HttpPost]
    public async Task<IActionResult> AddOpinion(DragonExpertOpinionModel dragonExpertOpinionModel)
    {
        await _dragonGraphClientFromNuget
            .AddOpinion(dragonExpertOpinionModel);

        return RedirectToAction("DragonDetail",
            new { id = dragonExpertOpinionModel.DragonId });
    }

    public async Task<IActionResult> AddOpinionOld(DragonExpertOpinionModel dragonExpertOpinionModel)
    {
        await _httpClient
            .AddOpinion(dragonExpertOpinionModel);

        return RedirectToAction("DragonDetail",
            new { id = dragonExpertOpinionModel.DragonId });
    }

Modyfikujemy widok DragonDetail.cshtml tak, aby dodać do niego nowy link do naszego formularza. 

@model DragonShop.Website.Models.DragonContainer;

<a asp-action="AddOpinion" asp-route-dragonId="@Model.Dragon.Id">Add a opinion</a>
<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>

    </div>
</div>

Swoją drogą tego widoku jeszcze nie mamy więc go dodajemy.

Dodanie nowego Widoku w ASP.NET CORE

W Widoku mam dwa formularze. Jeden wykona akcję POST do metody AddOpinion, a drugi AddOpinionOld

<h4>Add a opinion</h4>
<form asp-action="AddOpinion">
    <input type="hidden" asp-for="DragonId" />
    <div class="row mb-2 form-group">
        <div class="col-3">
            <label asp-for="Title"></label>
        </div>
        <div class="col-9">
            <input class="form-control" type="text" asp-for="Title" />
        </div>
    </div>
    <div class="row">
        <div class="col-3">
            <label asp-for="Review"></label>
        </div>
        <div class="col-9">
            <input class="form-control" type="text" asp-for="Review" />
        </div>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>
<form asp-action="AddOpinionOld">
    <input type="hidden" asp-for="DragonId" />
    <div class="row mb-2 form-group">
        <div class="col-3">
            <label asp-for="Title"></label>
        </div>
        <div class="col-9">
            <input class="form-control" type="text" asp-for="Title" />
        </div>
    </div>
    <div class="row">
        <div class="col-3">
            <label asp-for="Review"></label>
        </div>
        <div class="col-9">
            <input class="form-control" type="text" asp-for="Review" />
        </div>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

Pozostaje nam tylko przetestować formularz i sprawdzić czy nowe opinie trafią do bazy.

Formularz do dodawania opinii

Jak widać mamy nowe opinie.

Baza danych SQLite

To wszystko, jeśli chodzi o modyfikację danych. W następnym wpisie zobaczymy jak działa Subskrypcja.