środa, 25 stycznia 2023

Integracja z API Splunk

Splunk to narzędzie ułatwiające pracę z danymi produkowanymi przez aplikacje (chodzi tu głównie o procesowanie logów aplikacji). Łatwo możemy agregować dane, tworzyć alerty, raporty, bogate prezentacje stanu naszego systemu. Więcej informacji i szczegółowy opis znajduje się na stronie producenta oprogramowania. 

Jak większość obecnych narzędzi, Splunk udostępnia API przy pomocy którego możemy rozszerzyć dostępne funkcjonalności. Ten post zaprezentuje w jaki sposób wywołać zapytanie w Splunku i następnie pobrać rezultaty dla niego.

Wyszukiwanie realizujemy w 3 krokach. Za pomocą instrukcji POST wysyłamy zapytanie do przetworzenia. Wynikiem jest Id (sid) naszego zapytania, który następnie możemy użyć aby sprawdzić stan wykonania zapytania, jak i później pobrać rezultat. 

Najłatwiej cały proces zobrazować w kodzie:

using System.Text;
using System.Xml.Linq;

using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {EncodeAuthStringToBase64("splunk_user_name", "splunk_user_passowrd");

var queryResults = await GetQueryResultAsJson("search index=myindex earliest=-1m | stats count by host", httpClient);

Console.WriteLine(queryResults);


static string EncodeAuthStringToBase64(string userName, string password)
{
    var authenticationString = $"{userName}:{password}";
    return Convert.ToBase64String(Encoding.UTF8.GetBytes(authenticationString));
}

static async Task<string> GetQueryResultAsJson(string splunkQuery, HttpClient httpClient)
{
    const string splunkBaseAddress = "https://splunk_base_address:8089";
    var createSearchJobResult = await httpClient.PostAsync("{splunkBaseAddress}/services/search/jobs/", new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
    {
        new("search", splunkQuery)
    }));

    var readAsStringAsync = await createSearchJobResult.Content.ReadAsStringAsync();
    var xDocument = XDocument.Parse(readAsStringAsync);
    var querySid = xDocument.Root.Element("sid").Value;

    bool isQueryDone;
    do
    {
        await Task.Delay(100);
        var statusOfQuery =
            await httpClient.GetStringAsync(
                $"{splunkBaseAddress}/services/search/jobs/{querySid}");
        isQueryDone = statusOfQuery.Contains("<s:key name=\"dispatchState\">DONE</s:key>");
    } while (!isQueryDone);

    var queryResults =
        await httpClient.GetStringAsync(
            $"{splunkBaseAddress}/services/search/v2/jobs/{querySid}/results/?output_mode=json");

    return queryResults;
}


Kod nie jest skomplikowany, może jednak klika słów wyjaśnienia rozwieje wątpliwości:

1. Pierwszym krokiem jest stworzenie klienta za pomocą którego będziemy się komunikowali ze Splunkiem (zapytanie POST i GET)

2. Użyjemy Basic Auth

3. Za pomocą POSTa wysyłamy zapytanie do przetworzenia przez Splunk na adres /services/search/jobs/.

4. W odpowiedzi na zapytanie otrzymujemy unikalny SID naszego zapytania, który użyjemy do sprawdzenia stanu wykonania zapytania jak i pobrania rezultatów po jego ukończeniu. 

5. Cyklicznie odpytujemy Splunk o status naszego zapytania. Dla przykładu rozważamy optymistyczny scenariusz, że zapytanie na pewno się powiedzie. W kodzie produkcyjnym powinny być obsłużone także inne możliwe stany (QUEUED,PARSING,RUNNING,FINALIZING,DONE,PAUSE,INTERNAL_CANCEL,USER_CANCEL,BAD_INPUT_CANCEL,QUIT,FAILED)

6. Po pewnym czasie zapytanie zostanie prztworzone i możemy pobrać jego rezultat za pomocą sid z adresu /services/search/v2/jobs/{querySid}/results/?output_mode=json.

Na tym momencie możemy już dowolnie obrobić rezultat (który notabene może być także innym typem np. atom | csv | json | json_cols | json_rows | raw | xml).

wtorek, 24 stycznia 2023

AutoMapper w ASP.NET Core API

 Mapowanie klas modelu do obiektów DTO można zautomatyzować przy pomocy np. AutoMappera. Sama biblioteka nie jest już nowością na rynku i liczy sobie ponad 10 lat. Integracja z ASP.NET Core API jest bardzo prosta. Poniżej instrukcja:

1. Instalujemy dwie paczki:

  • AutoMapper
  • AutoMapper.Extensions.Microsoft.DependencyInjection


2. Tworzymy plik zawierający konfigurację mapowania

using AutoMapper;
using BookStoreApi.Models;
using BookStoreApi.Models.Dto;

namespace BookStoreApi;

public class MappingConfiguration : Profile
{
    public MappingConfiguration()
    {
        CreateMap<Book, BookDto>().ReverseMap();
    }
}

Metoda ReverseMap() pozwala na konfigurację mapowania zarówno z typu Book na BookDto jak i z obiektu BookDto na Book. Jeżeli nie potrzebujemy konwersji w drugą stronę możemy pominąć jej wywołanie.


3. W pliku Program.cs dodajemy do kontenera DI AutoMapper:

builder.Services.AddAutoMapper(typeof(MappingConfiguration));



4. Ostatnim krokiem jest wstrzyknięcie interfejsu IMapper do kontrolera i użycie metody Map. Przykład:
[ApiController]
[Route("api/[controller]")]
public class BookStoreController : ControllerBase
{
    private readonly BookStoreDbContext _bookStoreDbContext;
    private readonly IMapper _mapper;

    public BookStoreController(BookStoreDbContext bookStoreDbContext, IMapper mapper)
    {
        _bookStoreDbContext = bookStoreDbContext;
        _mapper = mapper;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
    {
        return Ok(_mapper.Map<List<BookDto>>(await _bookStoreDbContext.Books.ToListAsync()));
    }
}

Rezultat po uruchomieniu aplikacji będzie następujący:



poniedziałek, 23 stycznia 2023

Użycie Sqlite w aplikacji ASP.NET Core API wraz z Entity Framework Core

 W tym poście pokaże, jak łatwo podpiąć się pod bazę Sqlite z projektu ASP.NET Core API. Sqlite nie wymaga instalowania oprogramowania serwera baz danych. Więcej na temat tego kiedy używać Sqlite można przeczytać na oficjalnej stronie.

1. Pierwszym krokiem aby użyć Sqlite wraz z Entity Framework Core jest instalacja odpowiednich nugetów (paczek). Najłatwiej to zrobić za pomocą managera Nuget dostępnego w Visual Studio. Klikamy prawym klawiszem myszy na projekt i następnie wybieramy opcję Manage NuGet Packages...:


Instalujemy następujące paczki:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Sqlite
  • Microsoft.EntityFrameworkCore.Tools


2. Następny krokiem jest dodanie ConnectionString do ustawień aplikacji. W pliku appsettings.json dodajemy wpis:

  "ConnectionStrings": {
    "DatabaseConnection": "Data Source=books.db"
  }

Pierwszy element to nazwa połączenia, drugi to właściwy ConnectionString. Plik konfiguracyjny będzie wyglądać tak po tej zmianie:


3. Jako, że mamy podejście Code First (czyli najpierw piszemy kod a na jego podstawie generujemy bazę danych) - tworzymy klasę modelu. Przykładowo klasa Book:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace BookStoreApi.Models;

public class Book
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    public string Author { get; set; }

    public int PublishYear { get; set; }

    public DateTime CreateDate { get; set; }
}

Na pole Id oraz Name nałożone są dodatkowe atrybuty:

  • Key - identyfikuje pole, które będzie kluczem głównym dla tabeli
  • DatabaseGeneratedOption.Identity - podczas tworzenia wiersza w bazie, baza danych automatycznie wygeneruje jego wartość (w tym przypadku klucz jest typu int więc będzie to kolejna wartość sekwencji)
  • Required - identyfikuje kolumnę, która musi zostać wypełniona aby wiersz został dodany do tabeli

Możliwych atrybutów oczywiście jest znacznie więcej. Dla potrzeby tego przykładu nie będzie użytych więcej. 


4. Tworzymy klasę odpowiedzialną za połączenie z bazą danych, tzw. DbCotext

using BookStoreApi.Models;
using Microsoft.EntityFrameworkCore;

namespace BookStoreApi.Data;

public class BookStoreDbContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>().HasData(
            new()
            {
                Id = 1,
                Name =
                    "C#: 3 books in 1 - The Ultimate Beginner, Intermediate & Advanced Guides to Master C# Programming Quickly with No Experience",
                Author = "Mark Reed",
                PublishYear = 2022,
                CreateDate = new DateTime(2023, 1, 1)
            },
            new()
            {
                Id = 2, Name = "C++ For Dummies 7th Edition", Author = "Stephen R. Davis", PublishYear = 2014,
                CreateDate = new DateTime(2023, 1, 2)
            },
            new()
            {
                Id = 3,
                Name = "How to Make a Video Game All By Yourself: 10 steps, just you and a computer",
                Author = "Matt Hackett",
                PublishYear = 2022,
                CreateDate = new DateTime(2023, 1, 3)
            });
    }
}

Metoda OnModelCreating nie jest obowiązkowa w implementacji. Pozwala zainicjować początkowe dane w bazie, co może nam się przydać do testów, jak i również możemy za jej pomocą wprowadzić dane słownikowe.  


5. Kolejnym etapem jest rejestracja DBContext w kontenerze DI (Dependency Injection). Rejestracji dokonujemy w pliku Program.cs:

// Add services to the container.

builder.Services.AddDbContext<BookStoreDbContext>(option =>
    option.UseSqlite(builder.Configuration.GetConnectionString("DatabaseConnection")));


6. Stworzymy teraz przykładową akcję pobierająca książki z bazy danych. 

[ApiController]
[Route("api/[controller]")]
public class BookStoreController : ControllerBase
{
    private readonly BookStoreDbContext _bookStoreDbContext;

    public BookStoreController(BookStoreDbContext bookStoreDbContext)
    {
        _bookStoreDbContext = bookStoreDbContext;
    }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
    {
        return Ok(await _bookStoreDbContext.Books.ToListAsync());
    }
}


7. Od strony kodu, mamy już wszystko co jest potrzebne. Kolejne kroki stworzą naszą bazę danych. Otwieramy Package Manager Console (View -> Other Windows -> Package Manager Console) 



8. W konsoli wprowadzamy komendę add-migration migration_name przykładowo:

add-migration CreateBookDatabase


Jeżeli komenda zakończy się sukcesem powinniśmy zobaczyć nowy katalog w naszym projekcie nazwany Migrations


9. Teraz przejdziemy do właściwego utworzenia bazy danych. Po stworzeniu migracji, musimy zaaplikować zmiany do bazy danych (niezależnie czy jest to Sqlite czy inny silnik bazodanowy). Służy do tego komenda update-database:



10. Wszystko! Możemy uruchomić projekt i zobaczyć w akcji jak działa połączenie z naszą bazą danych:



poniedziałek, 9 stycznia 2023

Partial Update - PATCH w ASP.NET Core API

Partial Update - czyli częściowa aktualizacja zasobów możliwa jest przy użyciu metody PATCH. Jak powinniśmy używać PATCH opisane jest na stronie JSON Patch | jsonpatch.com Polecam zwłaszcza zobaczyć na opis składni, która nie jest od razu oczywista. Sama operacja partial update jest prosta i bardzo elastyczna a większość logiki załatwia za nas odpowiednia biblioteka. 

Aby w łatwy sposób skorzystać z PATCH w projekcie:

1. Instalujemy paczkę nuget Microsoft.AspNetCore.Mvc.NewtonsoftJson:


2. Zmieniamy bibliotekę formatującą JSONa z System.Text.Json na NewtonsoftJson:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers()
    .AddNewtonsoftJson();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

3. Tworzymy akcję w kontrolerze:

    [HttpPatch("{id:int}")]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public IActionResult UpdatePartialBook(int id, JsonPatchDocument<BookDto> bookPatchObject)
    {
        if (bookPatchObject == null || id == 0)
        {
            return BadRequest();
        }

        var book = //Code to get book from DB (or any other data source)
        if (book == null)
        {
            return NotFound();
        }

        bookPatchObject.ApplyTo(book, ModelState);
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return NoContent();
    }


I właściwie tyle :)


Teraz możemy przetestować naszą metodę w akcji. Przykładowo zmienimy autora dla pierwszej książki:

Przed zmianą:


Wywołujemy partial update:


Po zmianie potwierdzamy, że aktualizacja się powiodła:

wtorek, 3 stycznia 2023

Zwrócenie lokalizacji do zasobu po jego stworzeniu - CreatedAtRoute

Zasoby w WEB API ASP.NET Core tworzy się za pomocą akcji POST. Przykładowy kod tworzący zasób (w tym przypadku książkę):

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookDto bookDto)
    {
        if (bookDto == null)
        {
            return BadRequest(bookDto);
        }

        if (bookDto.Id > 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError);
        }

        //Code to store book in data storage...

        return Ok(bookDto);
    }

Powyższy kod jest jak najbardziej poprawny i stworzy żądany zasób:


Czasami jednak potrzebujemy zwrócić lokalizację (link) do utworzonego zasobu. Z pomocą przychodzi metoda CreatedAtRoute. Pierwszym parametrem tej metody jest akcja którą chcemy wywołać. W tym przypadku zakładamy, że chcemy odesłać użytkownika do metody pozwalającej pobrać książkę po Id. HttpGet oprócz parametrów wejściowych pozawala nazwać akcję co następnie pozwoli jej użyć przy przekierowaniu. Tak więc nasza metoda GetBook powinna wyglądać następująco:

    [HttpGet("{id:int}", Name = nameof(GetBook))]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<BookDto>> GetBook(int id)
    {
        if (id <= 0)
        {
            return BadRequest();
        }
        //code...
     }
Następnie w metodzie HttpPost dokonujemy dwóch aktualizacji (obsługa HTTP Status Code 201 Created oraz wywołanie metody CreatedAtRoute):

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    [ProducesResponseType(StatusCodes.Status201Created)]
    public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookDto bookDto)
    {
        if (bookDto == null)
        {
            return BadRequest(bookDto);
        }

        if (bookDto.Id > 0)
        {
            return StatusCode(StatusCodes.Status500InternalServerError);
        }

        //Code to store book in data storage...

        return CreatedAtRoute(nameof(GetBook), new { id = bookDto.Id }, bookDto);
    }


Po uruchomieniu i przetestowaniu kodu w Headerach otrzymamy link do lokalizacji nowo utworzonego zasobu:


Otwierając ten link w przeglądarce możemy potwierdzić, że zasób został poprawnie stworzony:



poniedziałek, 2 stycznia 2023

Swagger Undocumented Status Code - dokumentacja kodów zwracanych przez akcje Controllera

Poniższy kawałek kodu obrazuje prostą metodę zwracającą pojedynczą książkę na podstawie jej identyfikatora. Jeżeli identyfikator jest mniejszy lub równy zero zwracamy błąd Http Status Code 400 (zakładamy w tym przypadku, że w naszej bazie danych książki posiadają identyfikatory od 1 w górę).

    [HttpGet("{id:int}")]
    public async Task<ActionResult<BookDto>> GetBook(int id)
    {
        if (id <= 0)
        {
            return BadRequest();
        }
        //... Code, code...
     }
Kod jest jak najbardziej poprawy. Co może nas zdziwić, to że uruchamiając aplikację i testując ją pojawi nam się dziwny komunikat o braku dokumentacji dla kodu 400:



Powyższy brak można rozwiązać za pomocą atrybutu ProducesResponseType. Za jego pomocą definiujemy możliwe rezultaty z naszej metody jak i możemy jawnie wskazać scheme którą zwraca nasz serwis (w naszym przypadku nie jest to potrzebne ponieważ zwracamy typ ActionResult<BookDto>>):

    [HttpGet("{id:int}")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<BookDto>> GetBook(int id)
    {
        if (id <= 0)
        {
            return BadRequest();
        }
        //...code, code...
    }


Uruchamiając ponownie aplikacje z powyższą modyfikacją informacja Undocumented nie pojawi się ponownie. Dodatkowo poniżej otrzymamy listę wszystkich możliwych kodów rezultatów




Różnica między ControllerBase i Controller (MVC vs API)

Tworząc aplikację MVC Core lub API Core możemy nasz Controller dziedziczyć z jednej z dwóch klas ControllerBase lub Controller

Ogólna zasada dziedziczenia jest następująca:

  • ControllerBase - klasa bazowa dla projektów API
  • Controller - klasa bazowa dla projektów MVC
Klasa Controller zawiera składowe specyficzne dla widoków MVC. Zdecydowanie w API nie będą one przydatne. Przykładowo udostępnia takie właściwości jak ViewBag, ViewData, TempData, itp.

Wyjątkiem jest sytuacja, kiedy zamierzamy wykorzystać te same Controllery w aplikacji MVC i API. Dziedziczenie po Controller jest wtedy uzasadnione.  

    public class ApiController : ControllerBase
    {

    }

    public class MvcController : Controller
    {

    }