{"id":805,"date":"2024-05-10T19:26:25","date_gmt":"2024-05-10T17:26:25","guid":{"rendered":"https:\/\/blog.adameczek.pl\/?p=805"},"modified":"2024-05-19T11:43:37","modified_gmt":"2024-05-19T09:43:37","slug":"azure-functions-v4-perypetie-z-table-storage","status":"publish","type":"post","link":"https:\/\/blog.adameczek.pl\/index.php\/2024\/05\/10\/azure-functions-v4-perypetie-z-table-storage\/","title":{"rendered":"Azure Functions v4. Perypetie z Table storage"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Jednym z rodzaj\u00f3w sk\u0142adnicy danych na platformie Azure jest Azure Table storage. Tabele tworzone bezpo\u015brednio w ramach Storage Account s\u0105 do\u015b\u0107 prostymi konstrukcjami (s\u0105 te\u017c inne nazywane <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/cosmos-db\/table\/faq\">Azure Cosmos DB for Table<\/a>, ale tymi si\u0119 nie tu nie zajmujemy). Nie oferuj\u0105 zbyt wiele, je\u015bli chodzi o tworzenie zapyta\u0144, nie maj\u0105 schematu i dysponuj\u0105 tylko jednym indeksem, no i nie pozwalaj\u0105 na tworzenie relacji. Maj\u0105 za to bardzo mocny atut: s\u0105 tanie. Opr\u00f3cz tego szybkie i pojemne (do pojemno\u015bci konta storage). Je\u015bli Twoje funkcje zadowol\u0105 si\u0119 tabelami, to skorzystanie z Table storage b\u0119dzie na pewno z korzy\u015bci\u0105 dla OPEXu projektu.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">W tym wpisie om\u00f3wimy sobie przyk\u0142ad api z wykorzystaniem funkcji. Api b\u0119dzie zawiera\u0142o CRUD dla listy zada\u0144 z persystencj\u0105 w Azure Table Storage. W ten spos\u00f3b b\u0119dzie troch\u0119 \u0142atwiej zorientowa\u0107 si\u0119, jaki jest efekt dzia\u0142ania funkcji, bo dostaniemy odpowied\u017a ze statusem \u017c\u0105dania.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Azure Table storage<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tabele s\u0105 s\u0142ownikami z kilkoma polami predefiniowanymi. Wida\u0107 je w interfejsie <code>ITableEntity<\/code><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>namespace Azure.Data.Tables\n{\n    public interface ITableEntity\n    {\n        string PartitionKey { get; set; }\n        string RowKey { get; set; }\n        DateTimeOffset? Timestamp { get; set; }\n        ETag ETag { get; set; }\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\"><code>Timestamp<\/code> jest czasem aktualizacji rekordu (UTC) aktualizowanym przez Azure. <code>ETag<\/code> ma zastosowanie do <em>optimistic concurrency<\/em>. Nie interesuj\u0105 nas one teraz. Pola <code>PartitionKey<\/code> i <code>RowKey<\/code> stanowi\u0105 klucz <em>composite key<\/em> naszego rekordu. Na tym kluczu jest automatycznie za\u0142o\u017cony indeks. <br>Wa\u017cne dla nas dane u\u017cytkownika znajduj\u0105 si\u0119 w dodatkowych polach w postaci klucz:warto\u015b\u0107. Za\u0142\u00f3\u017cmy, \u017ce funkcja ma odk\u0142ada\u0107 w Table storage list\u0119 zada\u0144. Nasza encja na potrzeby Table storage b\u0119dzie wygl\u0105da\u0142a tak, jak klasa <code>TodoTableEntity<\/code>. W ka\u017cdej chwili mo\u017cemy zmodyfikowa\u0107 t\u0119 encj\u0119 i np. doda\u0107 pole. Przy zapisie zmodyfikowanej encji w table storage zostanie ono dodane, przy czym w istniej\u0105cych ju\u017c wierszach warto\u015bci\u0105 b\u0119dzie <em>null<\/em>. Tak si\u0119 objawia termin <em>schema less<\/em>, kt\u00f3rym okre\u015bla si\u0119 Table storage.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">W\u0142asny model danych<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We wcze\u015bniejszej wersji funkcji i bibliotek do realizacji Table storage bindings mo\u017cna by\u0142o korzysta\u0107 z typu <code>TableEntity<\/code> i z niego wywodzi\u0107 typy pochodne. Najpierw typ znikn\u0105\u0142 z biblioteki  <em>Microsoft.Azure.Functions.Worker.Extensions.Storage<\/em>, a potem dostali\u015bmy uproszczony interfejs w bibliotece <em>Microsoft.Azure.Functions.Worker.Extensions.Tables<\/em>. Warto utworzy\u0107 sobie abstakcyjn\u0105  klas\u0105 bazow\u0119 implementuj\u0105c\u0105 ten interfejs, a z niej wywodzi\u0107 konkretne klasy dla naszych danych.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-file=\"TodoTableEntity.cs\" data-lang=\"C#\"><code>using Azure.Data.Tables;\n\npublic abstract class BaseTableEntity : ITableEntity\n{\n    public string PartitionKey { get; set; } = null!;\n    public string RowKey { get; set; } = null!;\n    public DateTimeOffset? Timestamp { get; set; }\n    public ETag ETag { get; set; }\n}\n\npublic class TodoTableEntity : BaseTableEntity\n{\n    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;\n    public string Name { get; set; } = null!;\n    public string? Description { get; set; }\n    public bool IsCompleted { get; set; }\n}<\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Rozdzia\u0142 hosta z funkcjami od runtime<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Funkcje mog\u0105 by\u0107 uruchamiane w tym samym procesie, co runtime (<em>in-process<\/em>), a od NET6 tak\u017ce w procesie workera hostowanym w aplikacji ASP.NET Core albo zwyk\u0142ej aplikacji NET Core. Taka odmiana nazywa si\u0119 <em>isolated <\/em>i ma t\u0119 m.in zalet\u0119, \u017ce mo\u017cna korzysta\u0107 z kontenera DI. Aplikacj\u0119 po prostu konfiguruje si\u0119 jak ka\u017cd\u0105 aplikacj\u0119 ASP.NET\/NET Core. Dla naszego przyk\u0142adu u\u017cyjemy frameworku ASP.NET Core i odmiany <em>isolated<\/em>.Trzeba si\u0119 z trybem <em>isolated <\/em>zaznajomi\u0107 tym bardziej, \u017ce odmiana <em>in-process<\/em> traci wsparcie w listopadzie 2026r.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wi\u0105zanie encji z tabel\u0105 \u2013 binding<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Encja TodoTableEntity implementuj\u0105ca ITableEntity b\u0119dzie automagicznie zawiera\u0142a interesuj\u0105ce nas dane, kiedy tylko b\u0119dziemy chcieli do nich si\u0119gn\u0105\u0107. Przy odczycie  (<em>input binding<\/em>) zwr\u00f3ci encj\u0119 zadania. Albo sama \u201esi\u0119 zapisze\u201d do tabeli, je\u015bli ewentualnie przyjdzie nam do g\u0142owy zapisa\u0107 nowe zadanie (<em>output binding<\/em>). Za magi\u0119 odpowiedzialny jest <em>binding<\/em>. Robi si\u0119 go prosto w teorii prosto. Czy tak jest zobaczymy omawiaj\u0105c CRUD.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Dodanie wiersza<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Poni\u017cej jest kod pogl\u0105dowy, jak mog\u0142oby wygl\u0105da\u0107 utworzenie nowego zadania.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>[Function(&quot;CreateTodo&quot;)]\npublic TodoTableEntity Add(\n    [HttpTrigger(AuthorizationLevel.Anonymous, &quot;post&quot;, Route = &quot;todo&quot;)] HttpRequest request),\n    [TableOutput(&quot;TableName&quot;, Connection = &quot;ConnectionString&quot;)])\n{\n    \/\/ deserialise request body and map json to entity\n    return TodoTableEntity\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Atrybut <code>HttpTriggerAttribute <\/code>mapuje \u017c\u0105dania <em>post:http\\\\myhost\\api\\todo<\/em>. Uruchamiana jest funckja <em>CreateTodo<\/em>. Funkcja wykonuje zadanie utworzenia encji a return wywo\u0142uje sekwencj\u0119 zdefiniowan\u0105 przez binding. Ten jest zdefiniowany przez <code>TableOutputAttribute<\/code>. W ten spos\u00f3b encja powinna zosta\u0107 zapisana w tabeli (pami\u0119tamy, \u017ce <code>TodoTableEntity <\/code>implementuje <code>ITableEntity<\/code>) . Za\u0142o\u017cenie jest takie, \u017ce bindings s\u0105 abstrakcj\u0105 uwalniaj\u0105c\u0105 programist\u0119 od szczeg\u00f3\u0142\u00f3w warstwy persystencji.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">W funkcji <em>CreateTodo<\/em> triggerem jest \u017c\u0105danie http. To powoduje niejawny binding <code>HttpResponse<\/code>, \u017ceby po \u017c\u0105daniu mog\u0142a nadej\u015b\u0107 odpowied\u017a. Mamy wobec tego dwa bindingi: tabela oraz odpowied\u017a http. Oba oczekuj\u0105 innych modeli. Rozwi\u0105zano to w nast\u0119puj\u0105cy spos\u00f3b: tworzy si\u0119 klas\u0119, w kt\u00f3rej parametry o typie oczekiwanym przez binding, s\u0105 udekorowane atrybutem, np <code>TableOutputAttribute<\/code>. Binding typu <code>HttpResponse<\/code>, jak wspomnia\u0142em wcze\u015bniej, nie wymaga atrybutu. Binding do tabeli ma te\u017c t\u0119 mi\u0142\u0105 w\u0142a\u015bciwo\u015b\u0107, \u017ce nie rzuca b\u0142\u0119dem, kiedy dostaje null zamiast instancji modelu. To pozwala odes\u0142a\u0107 status b\u0142\u0119du http bez powodowania problem\u00f3w ze strony Table storage.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-file=\"TodoResponseDTO.cs\" data-lang=\"C#\"><code>public class TodoResponseDTO(TodoTableEntity? entity, HttpResponse response)\n{\n    [TableOutput(TodoApi.TableName, Connection = &quot;AzureWebJobsStorage&quot;)]\n    public TodoTableEntity? Entity { get; } = entity;\n\n    public HttpResponse Response { get; } = response;\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">W kodzie wida\u0107 odwo\u0142anie do statycznej metody mapuj\u0105cej modele <code>AsTableEntity()<\/code>. Jest to rozwi\u0105zane standardowo, a ca\u0142y kod mo\u017cna obejrze\u0107 klikaj\u0105c w link na ko\u0144cu artyku\u0142u. Ostatecznie funkcja <em>CreateTodo<\/em> wygl\u0105da nast\u0119puj\u0105co<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>[Function(&quot;CreateTodo&quot;)]\npublic async Task<TodoResponseDTO> Add(\n    [HttpTrigger(AuthorizationLevel.Anonymous, &quot;post&quot;, Route = &quot;todo&quot;)] HttpRequest req)\n{\n    _logger.LogInformation(&quot;Create a todo&quot;);\n\n    var requestBody = await new StreamReader(req.Body).ReadToEndAsync();\n    var response = req.HttpContext.Response;\n\n    try\n    {\n        var data = JsonSerializer.Deserialize<TodoCreateDTO>(requestBody);\n        var todo = new Todo { Name = data!.Name };\n        response.StatusCode = StatusCodes.Status200OK;\n        await response.WriteAsJsonAsync(todo);\n        return new TodoResponseDTO(todo.AsTableEntity(), response);\n    }\n    catch\n    {\n        response.StatusCode = StatusCodes.Status400BadRequest;\n        return new TodoResponseDTO(null, response);\n    }\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">W ten spos\u00f3b mamy rozwi\u0105zany temat utworzenia zadania w naszym przyk\u0142adowym api. Nowe zadanie jest zapisanie w Azure Table storage.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img fetchpriority=\"high\" decoding=\"async\" width=\"961\" height=\"57\" src=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2024\/05\/image-1.png\" alt=\"\" class=\"wp-image-868\" srcset=\"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2024\/05\/image-1.png 961w, https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2024\/05\/image-1-300x18.png 300w, https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2024\/05\/image-1-768x46.png 768w\" sizes=\"(max-width: 961px) 100vw, 961px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Odczyt pojedynczego zadania z Table storage<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tu tak\u017ce mamy mo\u017cliwo\u015b\u0107 zastosowania wi\u0105zania. W za\u0142o\u017ceniu \u017c\u0105danie <em>get:http:\/\/myhost\/api\/todo\/bcbdc435bc2f43449c2404ec03b0d7de<\/em> powinno wywo\u0142a\u0107 obs\u0142ug\u0119 wi\u0105zania i w efekcie zwr\u00f3ci\u0107 encj\u0119 zadania. Ca\u0142a funkcja powinna wygl\u0105da\u0107 tak:<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>[Function(&quot;GetTodoById&quot;)]\npublic async Task<IActionResult> GetById(\n    [HttpTrigger(AuthorizationLevel.Anonymous, &quot;get&quot;, Route = &quot;todo\/{id}&quot;)] HttpRequest req,\n    [TableInput(&quot;TableName&quot;, PartitionKey, &quot;{id}&quot;, Connection = &quot;ConnectionString&quot;)]  TodoTableEntity todo)\n{\n    if(todo is not null)\n        return new OkObjectResult(existingRow.AsTodo());\n    \n    return new NotFoundResult();\n}<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">\u017beby jednak nie by\u0142o zbyt \u0142atwo, obs\u0142uga tego wi\u0105zania zawiera b\u0142\u0105d, kt\u00f3ry spo\u0142eczno\u015b\u0107 zg\u0142asza\u0142a kilka miesi\u0119cy temu (by\u0142o otwieranych kilka <em>issues<\/em>). Na po\u0142ow\u0119 maja 2024r. b\u0142\u0105d nadal wyst\u0119puje. Mo\u017cna zastosowa\u0107 trik, kt\u00f3ry ma pewn\u0105 wad\u0119 \u2013 zwraca wszystkie wiersze, kt\u00f3re mo\u017cemy sobie przefiltrowa\u0107, ale raczej tego nie chcesz. <\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Dygresja na temat koszt\u00f3w<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Pami\u0119taj, \u017ce najkorzystniejszy cenowo model hostowania funkcji  to <em>consumption model<\/em>. P\u0142acimy za zu\u017cycie, kt\u00f3re jest liczone jako iloczyn czasu wykonywania funkcji oraz u\u017cytej pami\u0119ci (funkcje same si\u0119 skaluj\u0105 w krokach 128MB). Je\u015bli przy ka\u017cdym odczycie zadania poci\u0105gniesz ich tysi\u0105c \u2013 nie ma problemu. Ale je\u015bli poci\u0105gniesz baz\u0119 10 mln u\u017cytkownik\u00f3w, to mo\u017ce zabole\u0107. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dlatego nie zastosujemy wi\u0105zania do encji, tylko wykorzystamy je do otrzymania obiektu <code>TableClient<\/code>, kt\u00f3ry pozwala pracowa\u0107 bezpo\u015brednio z SDK. Zwr\u00f3\u0107 uwag\u0119, \u017ce klient rzuci b\u0142\u0119dem, je\u015bli wiersz nie zostanie odnaleziony. Ten b\u0142\u0105d trzeba obs\u0142u\u017cy\u0107, \u017ceby zwr\u00f3ci\u0107 status 400. Je\u015bli tego nie zrobisz, to funkcja zwr\u00f3ci status 500, kt\u00f3y nie odzwierciedli tego, co zasz\u0142o. W przyk\u0142adzie nie obs\u0142uguj\u0119 innych b\u0142\u0119d\u00f3w ni\u017c <code>RequestFailedException <\/code>klienta<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-file=\"TodoApi.cs\" data-lang=\"C#\"><code>[Function(&quot;GetTodoById&quot;)]\npublic async Task<IActionResult> GetById(\n    [HttpTrigger(AuthorizationLevel.Anonymous, &quot;get&quot;, Route = &quot;todo\/{id}&quot;)] HttpRequest req,\n    [TableInput(TableName, PartitionKey, &quot;{id}&quot;, Connection = &quot;AzureWebJobsStorage&quot;)] TableClient todoTable,\n    string id)\n{\n    _logger.LogInformation(&quot;Getting todo by id {Id}&quot;, id);\n\n    TodoTableEntity existingRow;\n    try\n    {\n        var findResult = await todoTable.GetEntityAsync<TodoTableEntity>(PartitionKey, id);\n        existingRow = findResult.Value;\n    }\n    catch (RequestFailedException e) when (e.Status == 404)\n    {\n        return new NotFoundResult();\n    }\n    return new OkObjectResult(existingRow.AsTodo());\n}<\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Odczyt wszyskich zada\u0144<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">W kontek\u015bcie dygresji o kosztach sam tytu\u0142 jest prowokacyjny. Oczywi\u015bcie, w przypadku wi\u0119kszych zbior\u00f3w danych nie powinni\u015bmy wog\u00f3le doprowadza\u0107 do sytuacji \u0142adowania ca\u0142o\u015bci do pami\u0119ci. Do dyspozycji mamy na szcz\u0119\u015bcie funkcj\u0119 rozszerzaj\u0105c\u0105 <code>AsPages()<\/code>, kt\u00f3ra pozwala zdefiniowa\u0107 wielko\u015b\u0107 strony (domy\u015blnie jest to 1000). Niestety nie ma gwarancji, \u017ce wszystkie rodzaje wi\u0105za\u0144 obs\u0142u\u017c\u0105 ten parametr. Tabele obs\u0142uguj\u0105. Czyli mo\u017cna zaimplementowa\u0107 paginacj\u0119 zamiast zaci\u0105gania wszysktiego. Ja na potrzeby przyk\u0142adu i pokazania wi\u0105zania do Table storage pomijam ten aspekt.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-file=\"TodoApi.cs\" data-lang=\"C#\"><code>[Function(&quot;GetTodos&quot;)]\npublic async Task<IActionResult> Get(\n    [HttpTrigger(AuthorizationLevel.Anonymous, &quot;get&quot;, Route = &quot;todo&quot;)] HttpRequest req,\n    [TableInput(TableName, Connection = &quot;AzureWebJobsStorage&quot;)] TableClient todoTable)\n{\n    var todosCount = await todoTable.QueryAsync<TodoTableEntity>().CountAsync();\n    _logger.LogInformation(&quot;Getting todos. There is {Count} entries.&quot;, todosCount);\n\n    \/\/ Be careful, it loads all items into memory.\n    \/\/ QueryAsync<TodoTableEntity>().AsPages().... could be used\n    var todos = await todoTable.QueryAsync<TodoTableEntity>().ToArrayAsync();\n    return new OkObjectResult(todos.Select(Mappings.AsTodo));\n}<\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Pozosta\u0142e operacje CRUD<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pozosta\u0142y do zaprezentowania literki U  i D \u2013 aktualizacja i usuni\u0119cie zadania. W konte\u015bcie wi\u0105zania z Table storage nie ma tu nic nowego ponad to, co ju\u017c przedstawi\u0142em. Trzeba skorzyska\u0107 z SDK i obiektu <code>TableClient<\/code>. Ma on odpowiednie metody <code>UpdateEntity()<\/code> i <code>DeleteEntity()<\/code> oraz ich wersje asynchroniczne. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Przy aktualizacji mo\u017cemy po\u0142\u0105czy\u0107 pola (<code>TableUpdateMode.Update<\/code>) lub zast\u0105pi\u0107 (<code>TableUpdateMode.Replace<\/code>). W naszym przypadku w\u0142a\u015bciwe jest zastosowanie <code>TableUpdateMode.Replace<\/code>. Przekazuj\u0119 ETag encji odczytanej z tabeli. Je\u015bli oka\u017ce si\u0119, \u017ce pomi\u0119dzy odczytem a zapisem przez ten egzemplasz funkcji kto\u015b inny wykona aktualizacj\u0119 w inny spos\u00f3b, to wyst\u0105pi b\u0142\u0105d. Om\u00f3wienie <em>concurrency <\/em>mo\u017ce by\u0107 tematem innego artyku\u0142u.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>\/\/ update\nawait todoTable.UpdateEntityAsync(existingRow, existingRow.ETag, TableUpdateMode.Replace);<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Przy usuwaniu zadania nie interesuje nas wersja, kt\u00f3r\u0105 usuwamy. Dlatego przekazujemy <code>ETag.All<\/code> (ostatni fragment kodu poni\u017cej). Niezale\u017cnie od wszystkiego zadanie zostanie usuni\u0119te.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zwr\u00f3\u0107 uwag\u0119, \u017ce przy aktualizacji i usuwaniu TableClient nie rzuci b\u0142\u0119dem, je\u015bli nie odnajdzie wiersza. Przy aktualizacji sprawdzam w\u0142a\u015bciwo\u015b\u0107 <code>HasValue<\/code><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>\/\/ get entity for update\nvar findResult = await todoTable.GetEntityIfExistsAsync<TodoTableEntity>(PartitionKey, id);\nif (!findResult.HasValue)\n    return new NotFoundResult();<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Natomiast przy usuwaniu sprawdzam w\u0142a\u015bciwo\u015b\u0107 <code>Status<\/code>. Je\u015bli wiersz nie istnieje, to zwracany jest status http 404.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-csharp\" data-lang=\"C#\"><code>\/\/ delete\nvar response = await todoTable.DeleteEntityAsync(PartitionKey, id, ETag.All);\nif(response.Status == 404)\n    return new NotFoundResult();<\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Azurite<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"> Do dzia\u0142ania funkcje potrzebuj\u0105 Azure Storage account. Przy uruchamianiu lokalnym warto u\u017cy\u0107 <em>Azurite<\/em>. Opensource\u2019owy <em>Azurite <\/em>zast\u0119puje <em>Azure Storage Emulator<\/em>. Udost\u0119pnia bezp\u0142atne lokalne \u015brodowisko do testowania aplikacji korzystaj\u0105cych z<br>\u2013 Azure Blob,\u00a0<br>\u2013 Queue Storage,<br>\u2013 Table Storage<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"> Je\u015bli pracujesz z Visual Studio, to <em>Azurite <\/em>b\u0119dzie uruchomiony razem z aplikacj\u0105. W innym przypadku trzeba r\u0119cznie uruchomi\u0107 emulator. Sk\u0105d pobra\u0107 i jak zainstalowa\u0107 dowiesz si\u0119 ze strony <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/storage\/common\/storage-use-azurite?tabs=visual-studio%2Cblob-storage\">Azurite emulator for local Azure Storage development<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Warto jeszcze skonfigurowa\u0107 aplikcaj\u0119, aby korzysta\u0142a z emulatora. Odpowiada za to plik <em>local.settings.json,<\/em> a dok\u0142adnie klucz <em>AzureWebJobsStorage<\/em>. Ten klucz wpisujemy jako warto\u015b\u0107 parametru <em>Connection<\/em> w wi\u0105zaniach. Przy publikacji do chmury warto\u015bci\u0105 klucza b\u0119dzie connection string do naszego Azure Storage account.<\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-json\" data-file=\"local.settings.json\" data-lang=\"JSON\"><code>{\n  &quot;IsEncrypted&quot;: false,\n  &quot;Values&quot;: {\n    &quot;AzureWebJobsStorage&quot;: &quot;UseDevelopmentStorage=true&quot;,\n    &quot;FUNCTIONS_WORKER_RUNTIME&quot;: &quot;dotnet-isolated&quot;\n  }\n}<\/code><\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\">Podsumowanie<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Przedstawi\u0142em, jak mo\u017cna wykorzysta\u0107 tani Azure Table storage do przechowywania danych i jak te dane przekazywa\u0107 do i z funkcji. Aplikacj\u0119 z funkcjami przedstawi\u0142em w modelu <em>isolated<\/em>, gdzie s\u0105 one uruchamiane jako osobny proces. W przysz\u0142o\u015bci zamierzam zrobi\u0107 wpis na temat <em>Durable Functions<\/em>. W tym wcieleniu funkcje s\u0105 uruchamiane nie tylko przez triggery, ale przez dodatkow\u0105 funkcj\u0119 \u2013 orkiestrator. To ciekawe rozwi\u0105zanie u\u0142atwia kolejkowanie i uruchamianie funkcji r\u00f3wnolegle. Zaczyna to przypomina\u0107 mikroserwisy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je\u015bli chcesz sprawdzi\u0107, jak dzia\u0142a omawiany kod, to dzia\u0142aj\u0105ca solucja jest do pobrania z repo: <a href=\"https:\/\/github.com\/madameczek\/todoapi\">https:\/\/github.com\/madameczek\/todoapi<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Jednym z rodzaj\u00f3w sk\u0142adnicy danych na platformie Azure jest Azure Table storage. Tabele tworzone bezpo\u015brednio w ramach Storage Account s\u0105 do\u015b\u0107 prostymi konstrukcjami (s\u0105 te\u017c inne nazywane Azure Cosmos DB for Table, ale tymi si\u0119 nie tu nie zajmujemy). Nie oferuj\u0105 zbyt wiele, je\u015bli chodzi o tworzenie zapyta\u0144, nie maj\u0105 schematu i dysponuj\u0105 tylko jednym [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":817,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ocean_post_layout":"","ocean_both_sidebars_style":"","ocean_both_sidebars_content_width":0,"ocean_both_sidebars_sidebars_width":0,"ocean_sidebar":"0","ocean_second_sidebar":"0","ocean_disable_margins":"enable","ocean_add_body_class":"","ocean_shortcode_before_top_bar":"","ocean_shortcode_after_top_bar":"","ocean_shortcode_before_header":"","ocean_shortcode_after_header":"","ocean_has_shortcode":"","ocean_shortcode_after_title":"","ocean_shortcode_before_footer_widgets":"","ocean_shortcode_after_footer_widgets":"","ocean_shortcode_before_footer_bottom":"","ocean_shortcode_after_footer_bottom":"","ocean_display_top_bar":"default","ocean_display_header":"default","ocean_header_style":"","ocean_center_header_left_menu":"0","ocean_custom_header_template":"0","ocean_custom_logo":0,"ocean_custom_retina_logo":0,"ocean_custom_logo_max_width":0,"ocean_custom_logo_tablet_max_width":0,"ocean_custom_logo_mobile_max_width":0,"ocean_custom_logo_max_height":0,"ocean_custom_logo_tablet_max_height":0,"ocean_custom_logo_mobile_max_height":0,"ocean_header_custom_menu":"0","ocean_menu_typo_font_family":"0","ocean_menu_typo_font_subset":"","ocean_menu_typo_font_size":0,"ocean_menu_typo_font_size_tablet":0,"ocean_menu_typo_font_size_mobile":0,"ocean_menu_typo_font_size_unit":"px","ocean_menu_typo_font_weight":"","ocean_menu_typo_font_weight_tablet":"","ocean_menu_typo_font_weight_mobile":"","ocean_menu_typo_transform":"","ocean_menu_typo_transform_tablet":"","ocean_menu_typo_transform_mobile":"","ocean_menu_typo_line_height":0,"ocean_menu_typo_line_height_tablet":0,"ocean_menu_typo_line_height_mobile":0,"ocean_menu_typo_line_height_unit":"","ocean_menu_typo_spacing":0,"ocean_menu_typo_spacing_tablet":0,"ocean_menu_typo_spacing_mobile":0,"ocean_menu_typo_spacing_unit":"","ocean_menu_link_color":"","ocean_menu_link_color_hover":"","ocean_menu_link_color_active":"","ocean_menu_link_background":"","ocean_menu_link_hover_background":"","ocean_menu_link_active_background":"","ocean_menu_social_links_bg":"","ocean_menu_social_hover_links_bg":"","ocean_menu_social_links_color":"","ocean_menu_social_hover_links_color":"","ocean_disable_title":"default","ocean_disable_heading":"default","ocean_post_title":"","ocean_post_subheading":"","ocean_post_title_style":"","ocean_post_title_background_color":"","ocean_post_title_background":0,"ocean_post_title_bg_image_position":"","ocean_post_title_bg_image_attachment":"","ocean_post_title_bg_image_repeat":"","ocean_post_title_bg_image_size":"","ocean_post_title_height":0,"ocean_post_title_bg_overlay":0.5,"ocean_post_title_bg_overlay_color":"","ocean_disable_breadcrumbs":"default","ocean_breadcrumbs_color":"","ocean_breadcrumbs_separator_color":"","ocean_breadcrumbs_links_color":"","ocean_breadcrumbs_links_hover_color":"","ocean_display_footer_widgets":"default","ocean_display_footer_bottom":"default","ocean_custom_footer_template":"0","_jetpack_memberships_contains_paid_content":false,"ocean_post_oembed":"","ocean_post_self_hosted_media":"","ocean_post_video_embed":"","ocean_link_format":"","ocean_link_format_target":"self","ocean_quote_format":"","ocean_quote_format_link":"post","ocean_gallery_link_images":"off","ocean_gallery_id":[],"footnotes":""},"categories":[26],"tags":[25],"class_list":["post-805","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure","tag-azure","entry","has-media"],"jetpack_featured_media_url":"https:\/\/blog.adameczek.pl\/wp-content\/uploads\/2024\/05\/Azure.png","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/805","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/comments?post=805"}],"version-history":[{"count":67,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/805\/revisions"}],"predecessor-version":[{"id":932,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/posts\/805\/revisions\/932"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media\/817"}],"wp:attachment":[{"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/media?parent=805"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/categories?post=805"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.adameczek.pl\/index.php\/wp-json\/wp\/v2\/tags?post=805"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}