Azure Functions – zmienne środowiskowe oraz zarządzanie pamięcią

Witam w kolejnym wpisie na temat podstaw Azure Functions. Dzisiaj chciałbym szybko przybliżyć jak działają zmienne środowiskowe na środowiskach lokalnym oraz produkcyjnym.

Zarządzenie pamięcią

Azure Functions sprowadza się do tego, że wszystkie funkcje, które zostały uruchomione w jednym środowisku współdzielą pamięć. Dla przykładu mamy dwie funkcje HTTP Trigger: jedna to jest SetterHttpTrigger i ona zapisuje nam dane do zmiennej statycznej a druga funkcja GetterHttpTrigger pobiera nam wszystkie dane, które zapisaliśmy do tej zmiennej statycznej. Dla przykładu stwórzmy dwie wyżej wymienione funkcje w projekcie. (link do wpisu jak utworzyć nowy projekt i dodać funkcje: https://antyper.pl/2020/10/18/poczatki-z-azure-functions/)

Stworzyłem sobie taką klasę w projekcie, które ma możliwość dodawania dowolnego zdania i zwrócenia wszystkich dodanych wcześniej zdań.

Teraz spróbujmy zaimplementować tę metodę do dodawania zdania do pamięci. Kod naszej funkcji SetterHttpTrigger:

Jak można zauważyć, kod pobierania danych z „ciała” żądania http jest podobny do tego używanego w bibliotece express.

A tutaj kod funkcji zwracającej wszystkie zdania zapisane w pamięci:

Żeby wrócić wartości, trzeba to zrobić poprzez umieszczenie danych w zmiennej „context.res”. Właściwość „res” może przyjmować jako wartość „status” (status http),”body” (dane które chcemy zwrócić) oraz „headers” (nagłówki żądania HTTP)

Uruchommy nasz kod (npm start) oraz przetestujmy nasz kod w Postmanie:

Dodałem właściwość „sentence” jako ciało żądania, wartość tej właściwości powinna nam się zapisać w pamięci naszych funkcji.

Próbujmy wysłać takie żądanie. Funkcja powinna nam zwrócić status: 201 co oznacza, że zasób został stworzony. Teraz sprawdźmy czy nasz zasób jest dostępny w naszej drugiej funkcji:

Możesz powtarzać wysyłać żądania HTTP, żeby testować działanie

Jak widać nasz bardzo kreatywne zdanie zostało zwrócone 😀

Zmienne środowiskowe

Zmienne środowiskowe w środowisku developerskim

W naszym głównym folderze projektu po stworzeniu projektu, możemy zobaczyć plik „local.settings.json” w nim są przechowywane zmienne środowiskowe, które zostają zaczytywane do naszego środowiska. Dodatkowo wszystkie zmienne środowiskowe są dostępne dla wszystkich funkcji w środowisku uruchomieniowych.

Każda właściwość dodana do właściwości „Values” jest traktowana jako zmienna środowiskowa. Po dodaniu nowej zmiennej pamiętaj o restarcie aplikacji. Inaczej nowe wartości nie zostaną wczytane.

Możemy wypróbować wyświetlić zawartość naszej zmiennej środowiskowej.
Jeszcze wracając do pliku „local.settings.json” pewnie zastanawia was wartość: „AzureWebJobsStorage”, można tutaj dać „connection string” do naszego Azure Storage Account. Azure Storage Account między innymi daje możliwość stworzenia kolejek zadań dla naszych funkcji. Dodatkowo fajną funkcjonalością jest, że po podpięciu naszego chmurowego Azure Storage Account do funkcji lokalnych, można obsługiwać wiadomości z kolejki które na tą kolejkę chmurową trafiają.

Jak widać w logach pojawiła nam się wartość naszej zmiennej. Jeżeli używacie TypeScript’a i wyświetli wam się błąd kompilacji: „Cannot find name 'process’.” to zainstalujcie paczkę „@types/node” poprzez komendę „npm i –save-dev @types/node”

Zmienne środowiskowe w środowisku produkcyjnym

Aby dodać/edytować/usunąć zmienną środowiskową możemy to zrobić na dwa sposoby.

Wyklikując to przez portal azure

Wystarczy wejść w znaleźć naszą zasób z funkcją, przejść do zakładki „Configuration” i kliknąć „Add new application setting”

Przez narzędzie Azure Cli

Możemy także użyć narzędzia Azure Cli instalujące je zgodnie z instrukcją ze strony: https://docs.microsoft.com/pl-pl/cli/azure/install-azure-cli.


Po zainstalowaniu narzędzia wystarczy, że zalogujemy się poprzez komendę:
az login – zostaniemy przekierowani do przeglądarki i będziemy poproszeni o zalogowanie się.

Następnie wystarczy, że użyjemy jedną z komend:
– az functionapp config appsettings delete,
– az functionapp config appsettings list,
– az functionapp config appsettings set,

Oto przykład użycia komendy do dodawania nowej zmiennej środowiskowej:

az functionapp config appsettings set --name test-envs --resource-group Test-resource-group --settings "TestEnv=envTest"

Objaśnienie co zrobilismy:

  • parametr –name: jest to nazwa naszego zasobu, w tym przypadku naszego Azure Functions,
  • parametr –resource-group: jest to nazwa naszej grupy zasobów, gdzie znajduje się nasz zasób, w tym przypadku funkcja „test-envs”
  • parametr –settings: tutaj podajemy wartość naszej zmiennej środowiskowej w formacie klucz/wartość („Klucz=Wartość”)

Początki z Azure Functions

Co to jest Azure Functions?
Jest to technologia serverless, która dostępna jest w chmurze Azure. A czym to właściwie jest ten serverless? Kiedy pierwszy raz usłyszałem to słowo to miałem lekki mind fuck 😀

Wtf Mind Fuck GIF - Wtf MindFuck Explode - Discover & Share GIFs

Jak kod może działać bez serwera? No jak? Przecież gdzieś ten kod musimy w końcu uruchomić. Zagadnienie serverless po prostu oznacza, że z nas programistów spada odpowiedzialność za konfigurację i zarządzaniem serwerem. W teorii wrzucamy kod i to tyle.

Po słowie wstępu dzisiaj chciałbym przybliżyć konfigurację oraz pierwsze uruchomienie Azure Functions na środowisku na developerskim.

Instalacje narzędzia Azure Functions Core Tools oraz .Net Core SDK, znajdziesz na oficjalnej stronie Microsoftu -> Azure Functions Core Tools oraz .Net Core SDK.

Jeżeli zainstalowałeś już wyżej wymienione narzędzia to znaczy że jesteśmy gotowi do pracy!

Utwórz folder w którym chcesz trzymać swój projekt. Ja utworzyłem folder HelloWorld. Po przejściu do tego folderu w terminalu wywołaj komendę:
func init

menu kontekstowe z wyborem środowisk dla projektu

Do wyboru mamy języki w jakich możemy zacząć pracę. Ja wybrałem Node.

Możemy wybrać w jakim języku ma być utworzony projekt. Może to być TypeScript lub JavaScript.

Następnie uruchom komendę func new w folderze projektu. Pojawi się kolejne menu kontekstowe z różnymi rodzajami funkcji

Wybieramy funkcje HTTP Trigger i nadajemy nazwę HelloWorldHttpTrigger (w jednym projekcie możemy używać różnych rodzajów funkcji) następnie instalujemy zależności przy użyciu komendy: npm install. Już prawie mamy koniec, zostało nam odpalenie projektu!

Po wykonaniu wszystkich kroków powinniśmy zobaczyć taką strukturę plików i folderów.

Teraz wystarczy odpalić komendę: npm start. Projekt powinien nam się zbudować i wyświetlą się funkcje, które dostępne są w naszym projekcie. Mogę dodać, że komenda npm start, także nasłuchuje zmian i automatycznie przebudowuje i restartuje projekt.

Przy wybraniu HTTP Trigger zostanie wyświetlony adres URL pod którym możemy wywołać funkcję

Spróbujmy przejeść pod adres:
http://localhost:7071/api/HelloWorldHttpTrigger?name=Joe

Wynik wywołania funkcji

W następnym wpisie chciałbym poruszyć wejść głębiej w temat konfiguracji oraz trochę jak działają funkcję.

Tworzenie obszarów w .Net Core

Obszary aplikacji to byty, które mają nam oddzielić jedną logikę aplikacji od drugiej, np:

Mamy panel administratora oraz obszar aplikacji dla klienta. Jeżeli chcielibyśmy to zrobić w klasyczny sposób, to widoki oraz logika aplikacji dla klienta i administratora byłyby w jednym katalogu głównym aplikacji i razem ze sobą pomieszane, przez co struktura aplikacji staje się mniej czytelna i trudniejsza do zarządzania.

Do przykładu użyję wcześniejszego projektu z wpisu: https://antyper.pl/2019/04/02/sposoby-wyszukiwan-widokow-w-net-core-mvc/
Oto link do repozytorium:
https://github.com/pnkp/Tutorials/tree/master/ViewsTutorials/SearchingViews

Zaczniemy od dodania folderu o nazwie Admin w naszym katalogu głównym projektu. Będę bazował na przykładzie aplikacji administratora oraz klienta. Następnie musimy skonfigurować routing w metodzie Configure, która znajduję w klasie Startup. Metoda po modyfikacji wygląda tak:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseDeveloperExceptionPage();
    app.UseStatusCodePages();
    app.UseStaticFiles();
    app.UseMvc(options =>
    {
        options.MapRoute(
            name: "areas",
            template: "{area:exists}/{controller=Home}/{action=Index}"
        ); //dodanie nowego routingu
        
        options.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}"
        );
    });
}

Dodałem nowy routing dla naszej aplikacji.

Uwaga!
Musimy pamiętać, że kolejność routingu jest ważna. Framework dobiera adres url do pierwszego dostępnego wzoru. Jeżelibyśmy zamienili kolejność zdefiniowanych routingów, aplikacja może zadziałać nieoczekiwanie.

Najbardziej może nas zaciekawić nietypowy wzór dla naszego routingu:

"{area:exists}/{controller=Home}/{action=Index}"

Dodany jest nowy parametr {area:exists}. Oznacza on, że jeżeli wystąpi nazwa zdefiniowanego obszaru, wtedy framework zaczyta kontroler i akcję z wybranego obszaru. Reszta parametrów została bez zmian. Dla domyślnego kontrolera zdefiniowany jest kontroler Home a dla akcji –Index. Oczywiście można dowolnie modyfikować domyślne wartości dla kontrolera i akcji.

Przejdźmy dalej i utwórzmy nowy plik o nazwie
HomeController.cs w lokalizacji Areas/Admin/Controllers. Oto treść naszego nowego pliku:

using Microsoft.AspNetCore.Mvc;

namespace ViewsProject.Areas.Admin.Controllers
{
    [Area("Admin")]
    public class HomeController : Controller
    {
        public ViewResult Index() => View();
    }
}

Główną różnicą między kontrolerem, który wcześniej zdefiniowałem w projekcie jest to, że występuję atrybut [Area(„Admin”]. Bez tego znacznika nasz obszar nie mógłby istnieć.
Jak można zauważyć, w atrybucie jest przypisana wartość Admin. Jest to nazwa naszego obszaru, według której framework będzie szukał naszych kontrolerów oraz akcji.

Dla celów dydaktycznych dodałem jeden model oraz repozytorium.
W lokalizacji Areas/Models utworzyłem plik o nazwie Client.cs:

namespace ViewsProject.Areas.Admin.Models
{
    public class Client
    {
        public int ClientId { get; set; }
        public string ClientName { get; set; }
    }
}

Jest to prosty model do przechowywania danych o pewnej strukturze.

Następnie utworzyłem plik MemoryRepository.cs w lokalizacji Areas/Admin/Repository, oto ciało klasy:

using System.Collections.Generic;
using ViewsProject.Areas.Admin.Models;

namespace ViewsProject.Areas.Admin.Repository
{
    public class MemoryRepository
    {
        private readonly IDictionary<int, Client> _clients = new Dictionary<int, Client>();

        public MemoryRepository()
        {
            AddClient(new Client{ClientName = "Paweł"});
            AddClient(new Client{ClientName = "Krzysztof"});
        }
        
        public Client this[int id] => _clients[id];

        public IEnumerable<Client> Clients => _clients.Values;

        public Client AddClient(Client client)
        {
            var index = _clients.Count;
            client.ClientId = index;

            _clients[index] = client;

            return client;
        }
    }
}

Klasa MemoryRepository ma na celu przetrzymania danych w pamięci. Będziemy ją wykorzystywać do wyświetlania danych o klientach.

Musimy jeszcze zmodyfikować nasz kontroler, tak aby mógł operować na klasie MemoryRepository. Ciało kontrolera po modyfikacji:

using Microsoft.AspNetCore.Mvc;
using ViewsProject.Areas.Admin.Repository;

namespace ViewsProject.Areas.Admin.Controllers
{
    [Area("Admin")]
    public class HomeController : Controller
    {
        private readonly MemoryRepository _repository = new MemoryRepository();

        public ViewResult Index()
        {
            return View(_repository.Clients);
        }
    }
}

Jak widać, w kontrolerze zainicjowałem klasę MemoryRepository w zmiennej prywatnej. Później do metody View() przekazałem dane z klasy MemoryRepository do modelu widoku.

Celowo zainicjowałem klasę MemoryRepository bezpośrednio w zmiennej w klasie HomeController. Później będę modyfikował ten przykład do pokazania mechanizmu wstrzykiwania i cyklu życia zależności. 

Do naszego projektu potrzebujemy jakiegoś widoku. W katalogu Areas/Admin/Views utworzyłem plik o nazwie Index.cshtml.

@model IEnumerable<Client>
@{ Layout = null;}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Obszar administratora</title>
</head>
<body>
Obszar administratora.
Klienci:
<table>
  <tr>
    <th>Id klienta</th>
    <th>Nazwa klienta</th>
  </tr>
  @foreach (var client in Model)
  {
    <tr>
      <th>@client.ClientId</th>
      <th>@client.ClientName</th>
    </tr>
  }
</table>
</body>
</html>

Zwróćcie uwagę na kluczowe słowo, którym jest: @model. Po słowie kluczowym definiujemy typ modelu jaki ma być wykorzystany do wyświetlenia widoku. Dzięki takiemu zabiegowi nasze IDE bardzo dobrze radzi sobie z podpowiadaniem składni modelu, który jest wykorzystany w widoku.

Kolejną rzeczą jest wyświetlenie listy klientów w tabeli. Wykorzystałem do tego celu pętlę @foreach. Silnik Razor iteruje po modelu a my możemy dowolnie formatować nasze dane w widoku.

Ważna sprawa!
Słowo kluczowe "model" z małej litery służy do definiowania naszego modelu w widoku. Natomiast słowo "Model" z wielkiej litery przechowuje dane naszego modelu. Dlatego w pętli foreach użyłem słowa kluczowego "Model", ponieważ znajdują się tam potrzebne dane.

Małym usprawnieniem jest dodanie pliku _ViewImports.cshtml. Musimy go umieścić w lokalizacji Areas/Admin/Views.
Jest to plik odpowiedzialny za importowanie między innymi specjalnych znaczników pomocniczych oraz modelu.
Ja użyłem go do zdefiniowania modelu, dzięki temu nie muszę wpisywać całej ścieżki przestrzeni nazw. Oto wygląd pliku _ViewImports.cshtml:

@using ViewsProject.Areas.Admin.Models;

Podsumowanie

Jak można zauważyć na przykładzie, obszary są przydatne w podzieleniu projektu na odpowiedzialności oraz porządkuje strukturę projektu.

Cały zmodyfikowany kod jest dostępny pod adresem:

https://github.com/pnkp/Tutorials/tree/master/ViewsTutorials/SearchingViews

Sposoby wyszukiwań widoków w .NET Core MVC

W dzisiejszym wpisie chciałbym poruszyć temat wbudowanego wyszukiwania widoków w frameworku .Net Core.

Materiał tworzony jest w celach edukacyjnych oraz utrwalania sobie wiedzy

Do ćwiczeń potrzebujemy pusty projekt .Net Core MVC. Generujemy go w wybranych folderze za pomocą narzędzia CLI.

dotnet new web

Po chwili utworzy nam się nowy projekt aplikacji webowej. Możemy go uruchomić za pomocą komendy:

dotnet watch run

Komenda uruchamia nam wewnętrzny serwer środowiska .Net Core. A parametr watch , nasłuchuje zmian w plikach, które podlegają kompilacji i automatycznie buduje i uruchamia serwer.

Do ćwiczeń potrzebujemy skonfigurować odpowiednio klasę Startup.cs, która znajduję się w głównym folderze projektu. Dokładnie musimy zmodyfikować dwie metody: ConfigureServices oraz Configure. Metody te odpowiadają za konfigurację naszej aplikacji.

Ciało naszej funkcji wygląda teraz tak:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
}

W metodzie ConfigureServices włączyliśmy framework .Net Core przy użyciu metody AddMvc(), która jest dziedziczona z interfejsu IServiceCollection.

Kolejnym krokiem jest skonfigurowanie metody Configure,

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
     app.UseDeveloperExceptionPage();
     app.UseStatusCodePages();
     app.UseStaticFiles();
     app.UseMvc(options =>
     {
          options.MapRoute(
              name: "default",
              template: "{controller=Home}/{action=Index}/{id?}"
         );
     });
}
  • UseDeveloperExceptionPage – odpowiada za wyświetlanie strony podczas wywołania niespodziewanego wyjątku w naszej aplikacji, pozwala ona nam namierzyć w którym miejscu i dlaczego pojawił się nieoczekiwany problem,
  • UseStatusCodePages – odpowiada za wyświetlanie strony z kodami błędów HTTP, np: jeżeli system routingu nie znajdzie odpowiedniej ścieżki, wtedy dostaniemy stronę z zawartością „Status Code: 404; Not Found” oraz statusem HTTP 404,
  • UseStaticFiles – odpowiada za statyczne pliki dołączane do naszej witryny, np: jeżeli chcemy z poziomu projektu dołączyć pliku ze kodem JavaScriptu lub z plikami CSS.
  • UseMvc – metoda udostępnia nam funkcje lambda, w tym ćwiczeniu wykorzystam tą metodę do wystawienia routingu,

Teraz trochę dłużej o metodzie MapRoute:

W tym przeciążeniu metody MapRoute dodałem dwa parametry. Name odpowiada za nazwę naszego routingu, a parametr template jest odpowiedzialny za wzór naszego routingu. Według tego wzoru zostają dopasowane adresy url do naszych kontrolerów.

Jak możemy zauważyć, wzór naszego routingu wygląda jakoś nietypowo. W nawiasach klamrowych mamy właściwości controller oraz action. Są to słowa kluczowe, które odpowiadają za generowanie linków oraz przypisanie wartości domyślnych. Dla przykładu, w folderze projektu utworzę folder Controllers i umieszczę w nich plik HomeController, a o to ciało pliku:

using Microsoft.AspNetCore.Mvc;

namespace ViewsProject.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            ViewBag.Controller = nameof(HomeController);
            ViewBag.Action = nameof(Index);
            return View();
        }
    }
}

Konwencja nazewnictwa frameworka .Net Core MVC wymaga, żeby wszystkie kontrolery były umieszczone w folderze Controllers w katalogu głównym projektu, wtedy framework znajdzie sobie bez problemu ścieżkę do pliku kontrolera, dzięki zachowaniu konwencji. Konwencja także wymaga, żeby nazwy klas i plik kontrolera zawierała po słowie kluczowym słowo Controller, np: jak w naszym przykładzie HomeController albo ViewController. Jak można zauważyć, HomeController dziedziczy po bazowym kontrolerze w przestrzeni nazw Microsoft.AspNetCore.Mvc, który udostępnia szereg bardzo przydatnych metod i właściwości. Dzięki niemu nie musimy ręcznie implementować potrzebnych podstawowych metod.

Następnym krokiem będzie utworzenie ścieżki folderów w naszym projekcie Views/Home oraz utworzenie w nim pliku Index.cshtml.

@{ Layout = null;}

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Hello</title>
</head>
<body>
Wnętrze Kontrollera @ViewBag.Controller i akcji @ViewBag.Action
</body>
</html>

Rozszerzenie pliku .cshtml jest to rozszerzenie pliku dla silnika generowania HTML Razor, który jest wbudowany do frameworka .Net Core.

Teraz spróbuj uruchomić aplikację i wpisać adres url, który wyświetli Ci się w konsoli.

W twojej przeglądarce powinien się pojawić taki wynik:

Wnętrze Kontrollera HomeController i akcji Index

Metoda View(), która generuje widoki występuje w czterech przeciążeniach:

  • View()
  • View(Object)
  • View(String)
  • View(String, Object)

Pierwsze przeciążenie metody View() wyszukuje widok po schemacie Views/NazwaKontrolera/NazwaAkcji.cshtml lub w ścieżce Views/Shared/NazwaAkcji.cshtml . W przypadku kontrolera Home i akcji Index, będzie to ścieżka Views/Home/Index.cshtml. Dlatego jest ważna konwencja nazewnictwa oraz konwencja utrzymywania określonych ścieżek do plików aplikacji.

Drugie przeciążenie metody View(Object) wyszukuje identycznie ścieżkę do widoku jak w przykładzie wyżej, tylko dodatkowo dołącza model, który ma być wyświetlony w widoku.

Przeciążenie View(String) jest bardziej złożone, ponieważ potrafi znaleźć plik widoku, który dotyczy innego kontrolera. Wtedy wywołanie tej metody będzie wyglądać tak:
View(Views/Test/List.cshtml)
Musimy pamiętać, żeby podać rozszerzenie pliku, inaczej nasz widok nie zostanie wczytany oraz zostanie rzucony wyjątek.
Kolejnym przykładem jest użycie tego samego widoku w innej akcji kontrolera:

 HomeController
...
public ViewResult List()
 {
      ViewBag.Controller = nameof(HomeController);
      ViewBag.Action = nameof(List);
      return View("Index");
 }

Metoda View zacznie nam przeszukiwać ścieżkę Views/Home. Jeżeli znajdzie widok w folderze Views/Home to nam go wyświetli, jeżeli nie znajdzie pliku to zacznie szukać folderze Views/Shared. Jeżeli taki widok będzie się znajdował to go wyświetli. W sytuacji gdy chcemy skorzystać widoków z folderu Views/Shared, nie musimy podawać pełnej ścieżki ani rozszerzenia pliku. Wystarczy sama nazwa widoku. W lokalizacji Views/Shared powinny się znajdować widoki, które będą współdzielone między innymi kontrolerami.
Oto przykład takiej metody:

 HomeController
...
public ViewResult Shared()
 {
      ViewBag.Controller = nameof(HomeController);
      ViewBag.Action = nameof(List);
      return View("SharedView");
 }

Czwarte przeciążenie metody, to połączenie przeciążenia metody View(string) oraz View(object). Metoda w tym przeciążeniu potrafi nam dołączyć model do wybranego widoku.

Myślę, że pora zakończyć ten wpis, bo już się zrobił bardzo długi a to tylko widoki 🙂 Następny wpis będzie poświęcony obszarom w frameworku .Net Core MVC.

Wszelkie pliki związane z tutorialem można znaleźć pod adresem:
https://github.com/pnkp/Tutorials
oraz dotyczące tego wpisu pod adresem:
https://github.com/pnkp/Tutorials/tree/master/ViewsTutorials/SearchingViews