wtorek, 7 marca 2023

Pobranie listy instancji Google Cloud Platform za pomocą PowerShella

 Google Cloud Platform Team przygotował moduł PoweShell za pomocą którego możemy w łatwy sposób zarządzać różnymi serwisami platformy chmurowej. 

W tym poście pokaże jak pobrać listę instancji oraz kawałek kodu bonusowego pokazujący jak pobrać po jednym hoście z każdego Manage Instance Group. 

Na początek instalujemy moduł GoogleCloud:

 Install-Module GoogleCloud  

Komenda pozwalająca wyświetlić wszystkie instancje dla naszego projektu w GCP:

 Get-GceInstance -Project "project_Id"  

Zostaną wyświetlone wszystkie wirtualne maszyny (Compute Engine) stworzone w naszym projekcie. 

Przypuśćmy, że nasz projekt składa się z wielu Manage Instance Groups (MIG). Naszym zadaniem jest sprawdzić na jednej maszynie z danego MIGu np. wersje dll-ki. Chcemy więc napisać kod, który zwróci nam po jednej maszynie dla każdego MIGa. Przyjmijmy dodatkowo że nasze maszyny mają formę nazewnictwa które pozwala w łatwy sposób przyporządkować ją do MIGa. 

Możemy dla przykładu przyjąć, że przykładowa lista maszyn to:

 envirsvrin-f6mf  
 envirsvrin-fgmf  
 envirsvrin-igmf  
 envirjsrul-femf  
 envirjsrul-fe1f  
 envirsvleo-ftmf  
 envirsvleo-f5mf  

Patrząc na to zestawienie od 5-tego znaku, 5 znaków jest unikalnych dla danego MIGa. 

Moja propozycja rozwiązania tego zadania, to użycie słownika i przechowanie jednej maszyny dla każdego z migów. Dodatkowo pobrane maszyny sortujemy po dacie stworzenia - tak więc mamy pewność, że maszyna działa od jakiegoś czasu i nie powinno być problemu z połączeniem do niej. Czy jest to najbardziej optymalne rozwiązanie? Zapewne nie - bardziej eleganckim rozwiązaniem byłoby pobranie wszystkich MIGów a następnie pobranie po jednej maszynie z każdego z nich. 

 $allVmsInProject = Get-GceInstance -Project "projectId" | Sort-Object -Property TimeCreated  
 #store one Vm for each mig  
 $vmDictionary = @{}   
 $(foreach ($vm in $allVmsInProject) {  
   $vmDictionary[$vm.Name.Substring(5, 5)] = $vm.Name  
 })  
 foreach ($vm in $vmDictionary.Values) {  
   Write-Host $vm  
 }  

Po wykonaniu skryptu otrzymamy następujący rezultat:

 envirjsrul-fe1f  
 envirsvleo-f5mf  
 envirsvrin-f6mf  

poniedziałek, 6 marca 2023

Serializacja Protobol Buffers (protobuf) danych przy pomocy biblioteki protobuf-net

 Protocol Buffers jest mechanizmem serializacji stworzonym przez Google. Założenie projektu opierało się na tym aby ilość danych wygenerowanych była jak najmniejsza, szybkość działania jak największa oraz nie były uzależnione od języka programowania czy użytej platformy. 

Można by powiedzieć, że jest to XML/JSON tylko w dużo lepszym wydaniu. Platforma .NET oferuje wiele narzędzi, które pozwalają tworzyć klasy na podstawie plików .proto. Na rynku istnieje także ciekawa biblioteka protobuf-net, która pozwala stworzyć kontrakt serializowanej klasy podobnie jak to ma się np. w przypadku XmlSerializera (za pomocą atrybutów).

Najłatwiej pokazać zasadę działania biblioteki na przykładzie. 

Na początek instalujemy protobuf-net:


Następnie definiujemy klasę którą chcemy zserializować: 

 [ProtoContract]  
 public class Person  
 {  
   [ProtoMember(1)]  
   public int Id { get; set; }  
   [ProtoMember(2)]  
   public string FirstName { get; set; }  
   [ProtoMember(3)]  
   public string LastName { get; set; }  
   [ProtoMember(4)]  
   public string Address { get; set; }  
   public override string ToString()  
   {  
     return $"{nameof(Id)}: {Id}, {nameof(FirstName)}: {FirstName}, {nameof(LastName)}: {LastName}, {nameof(Address)}: {Address}";  
   }  
 }  

Warto w tym miejscu wspomnieć o kilku aspektach:

  • ProtoContract - atrybut serializowanej klasy
  • ProtoMember(int i) - atrybut który przypisujemy poszczególnym właściwością klasy. Ważne aby numery nie powtarzały się dla tego samego typu (nawet w przypadku gdy dziedziczymy po tej klasie musimy pamiętać aby numery były unikalne). Unikalność jest na poziomie typu. Oznacza to, że dla kolejnego typu możemy rozpocząć znowu od 1. Nie warto używać dużych numerów początkowych. Im wyższy numer tym więcej danych do zapisu. 
Identyfikator (numer przypisany w atrybucie ProtoMember) jest jednym z najważniejszych elementów. Możemy zmienić nazwę pola, jednak dopóki jego numer pozostaje taki sam nie będzie problem z deserializacją danych. 

Teraz kawałek kodu który odpowiada za serializację / deserializację struktury danych. Dla przykładu dane zostaną zapisane do pliku:


 Person person = new()  
 {  
   Id = 1,  
   FirstName = "Mariusz",  
   LastName = "Kowalski",  
   Address = "Kraków, Malborska 10"  
 };  
 //1. Serializing data  
 var fileName = "person.bin";  
 using (var file = File.Create(fileName))  
 {  
   Serializer.Serialize(file, person);  
 }  
 //2. Deserializing data  
 using (var fileStream = File.OpenRead(fileName))  
 {  
   var deserializedPerson = Serializer.Deserialize<Person>(fileStream);  
   Console.WriteLine(deserializedPerson);  
 }  
 Console.ReadKey();  

Zarówno serializacja jak i deserializacja używa w tym przypadku klasy Serializer z biblioteki protobuf-net. Pracujemy na standardowych strumieniach danych - możemy je przekierować do pliku, przechować w pamięci czy też skonwertować do tablicy bajtów i przesłać np. do Pub/Suba.

czwartek, 2 marca 2023

Pobranie wersji pliku w PowerShell

 Za pomocą PowerShell możemy w łatwy sposób pobrać wersję pliku (biblioteki). Może się do przydać np. gdy chcemy sprawdzić czy na maszynie na pewno wrzucona jest poprawna wersja. Inną sytuację kiedy może się przydać ta wiedza jest sytuacja kiedy operujemy na wielu aplikacjach i chcemy sprawdzić jakiej wersji biblioteki używa dana aplikacja. 

Na początek oczywiście musimy znaleźć interesującą nas bibliotekę:

 $files = Get-ChildItem -Path "$directory\*.dll" -Recurse -Filter $dllName  

Powyższy kod zwróci wszystkie znalezione pliki. Teraz pozostaje już sprawdzenie właściwości VersionInfo.FileVersion:

 foreach ($file in $files) {            
   Write-Host $file.VersionInfo.FileVersion  
 }  

Używając komendy Invoke-Command możemy powyższy przykład przekształcić do skryptu który przeszukuje wiele hostów i zapisuje rezultat do pliku

 $hostsList = "host1", "host2", "host3"  
 $directoriesToSearch = "C:\directory1", "C:\directory2"  
 $dllNameVersionYouWantToLog = "Aspnet.dll"  
 $report = "c:\temp\report.txt"  
 New-Item -Path $report -ItemType File  
 foreach ($vmHost in $hostsList) {  
   $result = Invoke-Command -ComputerName $vmHost -ArgumentList ($vmHost, $dllNameVersionYouWantToLog, $directoriesToSearch) -ScriptBlock {  
     Param ($vmHost, $dllName, $directoriesToSearch)  
     Write-Host $vmHost  
     $pathsWithVersion = New-Object -TypeName "System.Text.StringBuilder";  
     foreach($directory in $directoriesToSearch) {  
       if (Test-Path -Path $directory){  
         $files = Get-ChildItem -Path "$directory\*.dll" -Recurse -Filter $dllName  
         foreach ($file in $files) {  
           [void]$pathsWithVersion.AppendLine("$($vmHost);$($file);$($file.VersionInfo.FileVersion)")  
         }  
       }  
     }  
     return $pathsWithVersion.ToString()  
   }  
   if($result) {  
     Add-Content $report -Value "$($result)"  
   }  
 }  

Oczywiście nic nie stoi na przeszkodzie aby parametry do skryptu przesłać z lini komend, bądź wczytać z pliku. Jedynym ograniczeniem jest wyobraźnia :)

poniedziałek, 27 lutego 2023

GCP Metryki Pub/Sub

 Korzystając z SDK Google możemy w łatwy sposób pobrać metryki dla Pub/Sub.

Aby pobrać metryki dla projektu skorzystamy z biblioteki Google.Cloud.Monitoring.V3. Pozwala ona pobrać metryki dla większości serwisów oferowanych przez GCP. Nas w tym przypadku interesować będą metryki dla Pub/Suba. 

Spis metryk które możemy pobrać znajduje się w oficjalnej dokumentacji: https://cloud.google.com/monitoring/api/metrics_gcp#gcp-pubsub

Rozpoczynamy od instalacji biblioteki, które umożliwi pobranie metryk:


API dla jednego zapytania obsługuje pobranie tylko jednej metryki. Nie możemy zatem w jednym zapytaniu pobrać np. czasu najstarszej wiadomości jak i ilości wiadomości oczekujących na subskrypcji. 


 var metricServiceClient = await MetricServiceClient.CreateAsync();  
 var request = new ListTimeSeriesRequest  
 {  
   ProjectName = new ProjectName("project_id"),  
   Filter = "metric.type = \"pubsub.googleapis.com/subscription/oldest_unacked_message_age\"",  
   Interval = new TimeInterval  
   {  
     StartTime = Timestamp.FromDateTime(DateTime.UtcNow.AddMinutes(-10)),  
     EndTime = Timestamp.FromDateTime(DateTime.UtcNow)  
   },  
   View = ListTimeSeriesRequest.Types.TimeSeriesView.Full  
 };  
 var results = metricServiceClient.ListTimeSeriesAsync(request);  
 await foreach (var result in results)  
 {  
   Console.WriteLine(result.Resource.Labels["subscription_id"]);  
   foreach (var point in result.Points)  
   {  
     Console.Write(point.Interval.StartTime);  
     switch (point.Value.ValueCase)  
     {    
       case TypedValue.ValueOneofCase.BoolValue:  
         Console.WriteLine(point.Value.BoolValue);  
         break;  
       case TypedValue.ValueOneofCase.Int64Value:  
         Console.WriteLine(point.Value.Int64Value);  
         break;  
       case TypedValue.ValueOneofCase.DoubleValue:  
         Console.WriteLine(point.Value.DoubleValue);  
         break;  
       case TypedValue.ValueOneofCase.StringValue:  
         Console.WriteLine(point.Value.StringValue);  
         break;  
       case TypedValue.ValueOneofCase.DistributionValue:  
         Console.WriteLine(point.Value.DistributionValue);  
         break;  
       default:  
         throw new ArgumentOutOfRangeException();  
     }  
   }  
   Console.WriteLine(new string('-', 50));  
 }  

Wynik po wykonaniu kodu:


Otrzymany wynik zgodnie z dokumentacją podany jest w sekundach. 

A teraz przykład jak odfiltrować subskrypcję po jej nazwie (subscription_id).


 request = new ListTimeSeriesRequest  
 {  
   ProjectName = new ProjectName("project_id"),  
   Filter = "metric.type = \"pubsub.googleapis.com/subscription/num_undelivered_messages\" AND (resource.label.subscription_id = starts_with(\"aaa.\") OR resource.label.subscription_id = starts_with(\"bbb.\"))",  
   Interval = new TimeInterval  
   {  
     StartTime = Timestamp.FromDateTime(DateTime.UtcNow.AddMinutes(-1)),  
     EndTime = Timestamp.FromDateTime(DateTime.UtcNow)  
   },  
   View = ListTimeSeriesRequest.Types.TimeSeriesView.Full  
 };  

Oczywiście jeżeli nie chcemy wykonywać operacji typu like możemy od razu podać nazwę subskrypcji.

Pobrać możemy dowolną metrykę z oficjalnej specyfikacji. Metryki mogą być agregowane w różne okienka czasowe (minutowe, lub dłuższe). Za pomocą parametru Interval kontrolujemy przedział czasowy otrzymanych wyników. 

wtorek, 21 lutego 2023

Pobranie listy instancji w GCP

Google Cloud Platform oferuje gotowe SDK dla .NET aby automatyzować (jak i programistyczne wykorzystać) manuale zadania. 

Przydatną funkcję może być pobranie dostępnej listy Manage Instance Groups z GCP (np. w celu późniejszego pobrania maszyn wirtualnych dla każdego z MIGów).


Pierwszym krokiem jest dodanie biblioteki Google.Cloud.Compute.V1 do projektu:


Następnie kod który pozwoli dla danego projektu odczytać wszystkie Manage Instance Groups:

InstanceGroupManagersClient instanceGroupManagersClient = await InstanceGroupManagersClient.CreateAsync();
var pagedAsyncEnumerable = instanceGroupManagersClient.AggregatedListAsync("project_id");
await foreach (var zoneMigs in pagedAsyncEnumerable)
{
    foreach (var instanceGroup in zoneMigs.Value.InstanceGroupManagers)
    {
        Console.WriteLine(instanceGroup.Name);
    }
}


Korzystanie z gotowych bibliotek SDKa niesamowicie ułatwia kodowanie. API wymagałoby znacznie większej ilości kroków aby uzyskać ten sam efekt. 

poniedziałek, 6 lutego 2023

IIS Tracing - czyli jak debugować problemy, które nie zostawiają śladów w logach

 Pracując z produkcyjnymi aplikacji hostowanymi na IISie możemy napotkać na trudności z błędami typu 500. Błędy te są trudne w debugowaniu, zwłaszcza, gdy pojawiają się sporadycznie a w aplikacji nie ma wystarczającego logowania, które przechwytuje wszystkie wyjątki. 

Pomóc w takim przypadku może opcja Tracingu dostępna w serwerze IIS. Na początek należy sprawdzić czy potrzebne funkcje są zainstalowane:


Po instalacji Tracingu w IIS - przystępujemy do konfiguracji. Aktywować Tracing możemy zarówno poprzez klikanie w UI IISa, jak i dużo łatwiej i szybciej za pomocą PowerShella (to rozwiązane także łatwo zautomatyzować w przypadku dużej ilości serwerów). 

1 Sposób - UI IIS Console

Pierwszy sposób polega na wyklikaniu wszystkiego w interfejsie graficznym. Konfigurację możemy dokonać na poziomie Serwera lub konkretnej aplikacji. Jeżeli dokonamy konfiguracji na poziomie Serwera, wszystkie strony będą niejako dziedziczyły ją. Zaczniemy od zdefiniowania kryteriów logowania, następnie włączymy Tracking i ustawimy gdzie i ile plików ma zostać zapisane. 



W nowym okienku klikamy po prawej stronie w manu Actions -> Add... Otworzy się kreator, w którym możemy dokonać wyboru opcji. W pierwszym kroku możemy wybrać co chcemy logować:

W kolejnym kroku wybieramy warunki logowania (np. wszystkie wiadomości które zakończyły się błędem 500):

Ostatni krok pozwala wybrać poziom prowidera i poziom logowania:

Polecam na początek ustawić logowanie na wszystko na poziome Verbose. Pliki logów nie zajmują dużo a więcej detali ułatwi szukanie odpowiedzi dlaczego aplikacja nie działa poprawnie.

Ostatnim krokiem jest właściwie włączenie Tracingu na Site. Podobnie jak poprzednio wykorzystamy UI IISa:
Po lewej strony z menu wybieramy interesujący nas Site. Następnie po prawej stronie Configuration -> Failed Request Tracking...

W nowym okienku wybieramy:
  • zaznaczamy Enable aby włączyć logowanie
  • Directory - miejsce gdzie zostaną zapisane logi
  • Maximum number of trace files - 50 logów wydaje się nie dużą ilością, polecam ustawić co najmniej 1000 logów

Po zaakceptowaniu ustawień, wszystko jest gotowe i możemy czekać na pierwsze logi. 

2 Sposób - PowerShell

Moim zdaniem dużo prostszy i łatwiejszy sposób konfiguracji. Zacznijmy od komendy która dodaje Tracing na błędy z kodem 500 (czyli de fakto to co poprzednio wyklikiwaliśmy ręcznie):
Enable-WebRequestTracing -StatusCodes 500 -MaxLogFiles 2000

Jeżeli chcemy wskazać dokładnie jeden Site do tracowania wystarczy dodać parametr -Name "Site Name"

Do wyłączenia tracingu służy komenda:

Disable-WebRequestTracing

Tak samo możemy wyspecyfikować Site na którym chcemy wyłączyć tracing za pomocą parametru -Name. Wyłączenie tracingu nie powoduje usunięcia jego ustawień. Aby wyczyścić wszystkie ustawienia, należy skorzystać z komendy:

Clear-WebRequestTracingSettings


Przeglądanie plików Traca

Pliki traców zapisywane są jako pliki typu XML wraz z jednym plikiem transformacji XSL.

Otworzyć je możemy tylko po uprzednim zahostowaniu ich na serwerze WWW. Jeżeli spróbujemy otworzyć plik bezpośrednio w przeglądarce zobaczymy błąd bezpieczeństwa. 

Ja użyłem IISa aby zahostować plik i to wynik po nałożeniu formatowania:

Logi traca dostarczają wielu przydatnych informacji, jak:
  • zdarzenia dla każdego modułu, który brał udział przy procesowaniu requestu
  • który moduł zawiódł
  • hedery z requesta
  • body requesta
  • response wychodzący do klienta

W przypadku powyżej po anallizie, okazało się, że request zawierał niedozwolone znaki dla XMLa. Bez możliwości szybkiego wrzucenia nowej wersji kodu z odpowiednim logowaniem, trace wydaje się idealnym narzędziem pozwalającym zidentyfikować problem. 

ś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: