.net Core API – nahrání souborů a json objektů

Dnes se podíváme na problém, se kterým se můžete setkat v .net core API.

Zadání: založte POST endpoint, který přijme pole souborů a pole objektů (informace pro každý soubor).

Zní to jednoduše, však řešení má trochu háček. pojďme si to ukázat. Založme request:

using System.ComponentModel.DataAnnotations;

namespace ApiExample.Dtos
{
    public class SnapRequest
    {
        /// <summary>
        /// Metadata collection
        /// </summary>
        [Required]
        public required IEnumerable<Metadata> Metadatas { get; set; }

        /// <summary>
        /// Files collection
        /// </summary>
        [Required]
        public required IEnumerable<IFormFile> Files { get; set; }
    }
}

Akce v kontroleru:

using ApiExample.Dtos;
using Microsoft.AspNetCore.Mvc;

namespace ApiExample.Controllers
{
    public class SnapController : Controller
    {
        [HttpPost("create")]
        public IActionResult Create(SnapRequest snapRequest)
        {
            return Ok(snapRequest);
        }
    }
}

Výsledek ve Swaggeru vypadá takto:

Metadata se automaticky vložily do query, což může být problém, protože query je limitované maximální délkou (defaultně 260znaků). Všimněte si i toho, že když vyplníte například 2 metadata a vložíte 2 soubory po zpracování požadavku v akci kontroleru máte 0 metadat a 2 soubory 🙂

FormForm

Pojďme to torchu vylepšit. Query parametry jsou vhodné pro krétké informace, nejsou vhodné pro pole objektů. Pro pole objektů je bhodnější body, ale tam už máme soubory. Pošleme si tedy metadata přes FromForm:

using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ApiExample.Dtos
{
    public class SnapRequest
    {
        /// <summary>
        /// Metadata collection
        /// </summary>
        [Required]
        [FromForm]
        public required IEnumerable<Metadata> Metadatas { get; set; }

        /// <summary>
        /// Files collection
        /// </summary>
        [Required]
        [FromBody]
        public required IEnumerable<IFormFile> Files { get; set; }
    }
}

Kontroler:

using ApiExample.Dtos;
using Microsoft.AspNetCore.Mvc;

namespace ApiExample.Controllers
{
    public class SnapController : Controller
    {
        [HttpPost("create")]
        public IActionResult Create([FromForm] SnapRequest snapRequest)
        {
            return Ok(snapRequest);
        }
    }
}

Nyní už i Swagger vypadá trochu lépe. Všechny property se posálají v těle (body) kde máme podstatně vyšší limity než v query.

Ale stále nemáme splněno. Když zkusíte Swagger vyplnit, soubory jsou OK ale stále se do akce nedostanou žádná metada. Mám pro vás 2 možnosti řešení.

Request.Form

Problém spočívá v tom, že C# nedokáže rozparsrovat požadavek a napasovat ho na IEnumerable<Metadata>. Pojďme se podívat co nám přijde:

Ano, vidíte správně. Přišla nám informace:

{
  "name": "string1",
  "sjz": "string1"
},{
  "name": "string2",
  "sjz": "string2"
}

Vzpad8 to jako JSON ale není to JSON? Ano, přesně tak. Aby to byl JSON musely by na začátku a na konci být hranaté zévotky [ požadavek ]

Tím se dostáváme k prvnímu možnému řešení. Doplňme a začátek a konec záavorky a deseralizujme na kolekci objektů:

        [HttpPost("create")]
        public IActionResult Create([FromForm] SnapRequest snapRequest)
        {

            var metadatas = HttpContext.Request.Form["metadatas"];
            var metadataJson = $"[{metadatas}]";//Add [ to start a ] to end string -> it is true JSON array

            var options = new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            };
            //deserialize with camelCaseInsensitive option and save to request object
            snapRequest.Metadatas = JsonSerializer.Deserialize<List<Metadata>>(metadataJson, options) ?? new List<Metadata>();

            return Ok(snapRequest);
        }

ModelBinder

První řešení je spíše nouzové, lepší je si napsat vlastní ModelBinder, který tuto funkcionalitu zapouzdří.

Akce v kontroleru se velice zjednoduší:

        [HttpPost("v2/create")]
        public IActionResult CreateV2([FromForm] SnapRequestV2 snapRequest)
        {
            return Ok(snapRequest);
        }

DTO požadavku jsou rozšířené o ModelBiner:

using ApiExample.Controllers;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ApiExample.Dtos
{
    public class SnapRequestV2
    {
        /// <summary>
        /// Metadata collection
        /// </summary>
        [Required]
        [FromForm]
        [ModelBinder(BinderType = typeof(FormDataJsonArrayModelBinder))]
        public required IEnumerable<Metadata> Metadatas { get; set; }

        /// <summary>
        /// Files collection
        /// </summary>
        [Required]
        [FromBody]
        public required IEnumerable<IFormFile> Files { get; set; }
    }
}

Vytvoření vlastního ModelBinderu v ASP.NET Core je užitečné, pokud potřebujete specifickou logiku pro vazbu dat z HTTP požadavku na objekty v rámci vaší aplikace. Vlastní ModelBinder vám umožní upravit proces vazby dat podle vašich potřeb. Vytvoříme třídu FormDataJsonArrayModelBinder implementující rozhraní IModelBinder:

    public class FormDataJsonArrayModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult != ValueProviderResult.None)
            {
                bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

                var stringValue = $"[{valueProviderResult.Values}]";
                var result = Newtonsoft.Json.JsonConvert.DeserializeObject(stringValue, bindingContext.ModelType);
                if (result != null)
                {
                    bindingContext.Result = ModelBindingResult.Success(result);
                    return Task.CompletedTask;
                }
            }

            return Task.CompletedTask;
        }
    }

Tato třída stejně jako v prvním řešení přidá na začátek a konec dat hranaté závorky a deserializuje je na požadovaný objekt.

Redis v kontejneru

Redis (zkratka z anglického „Remote Dictionary Server“) je open-source in-memory key-value úložiště (NoSQL databáze) s otevřeným zdrojovým kódem. Redis byl původně vytvořen jako rychlý a efektivní způsob ukládání a získávání dat v paměti (in-memory database) pro zlepšení výkonu webových aplikací. Často se používá pro ukládání cache.

In-Memory storage: Redis ukládá data přímo v paměti, což ho dělá extrémně rychlým. To z něj činí vhodný nástroj pro cache, rychlé ukládání a získávání dat a další aplikace, které vyžadují nízkou latenci.

Key-Value store: Redis ukládá data ve formátu klíč-hodnota, to znamená, že každý záznam má jedinečný klíč, pod kterým je uložen. Data jsou obvykle binární nebo textové, a Redis umožňuje rychlé vyhledávání a získávání dat na základě klíče.

Podpora datových struktur: Redis nepodporuje pouze jednoduché hodnoty, ale také různé datové struktury jako seznamy, množiny, hashovací mapy a další. To umožňuje složitější operace s daty a efektivní manipulaci s nimi.

Pub/Sub mechanismus: Redis umožňuje publikování a odběr zpráv, to je užitečné pro asynchronní komunikaci mezi částmi aplikace.

Perzistence dat: Redis umožňuje volitelnou perzistenci dat na disk, což zajišťuje, že data nejsou ztracena po restartu serveru.

Clustering: Redis podporuje klustering, to umožňuje škálovat databázi na více serverů a zlepšit výkon a dostupnost.

Redis je často používán pro ukládání chache, asynchronní fronty zpráv, čítačky stavů a mnoho dalších aplikací, které vyžadují rychlé ukládání a získávání dat. Jeho jednoduchý a efektivní design ho činí oblíbeným nástrojem pro vývojáře v celé řadě aplikací a scénářů.

Založíme soubor docker-compose.yml :

version: '3.8'

services:
  redis:
    image: "redis:latest"
    container_name: redis
    ports:
      - "6379:6379"

Spustíme příkazem: docker-compose up nebo na pozadí příkazem: docker-compose up -d

RabbitMQ v kontejneru

Dnes si ukážeme jak jednoduše rozject RabbitMQ v dokcer kontejneru. Pro orchestraci použijeme docker compose.

Založíme soubor docker-compose.yml :

version: '3.4'

services:

  rabbitmq:
    image: rabbitmq:3-management
    hostname: "rabbitmq"
    labels:
      NAME: "rabbitmq"
    ports:
      - "4369:4369"
      - "5671:5671"
      - "5672:5672"
      - "25672:25672"
      - "15671:15671"
      - "15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: "username"
      RABBITMQ_DEFAULT_PASS: "password"

Spustíme příkazem: docker-compose up nebo na pozadí příkazem: docker-compose up -d

RabbitMQ je message broker, který používá protokol AMQP (Advanced Message Queuing Protocol) k usnadnění komunikace mezi různými aplikacemi. RabbitMQ běží na určitých síťových portech, které jsou důležité pro jeho funkčnost a komunikaci. Zde jsou některé z portů, které RabbitMQ používá:

4369: Tento port je používán pro Erlang Port Mapper Daemon (EPMD). EPMD je procesový démon, který umožňuje komunikaci mezi různými uzly v distribuovaném Erlangu, což je jazyk, na kterém je postaven RabbitMQ.

5671: Tento port je také asociován s AMQP, ale používá zabezpečený protokol SSL/TLS pro šifrovanou komunikaci. Je vhodné použít tento port pro bezpečnou komunikaci přes AMQP.

5672: Toto je výchozí port pro běžné spojení AMQP v RabbitMQ. Pokud není specifikován jiný port, klienti budou komunikovat přes tento port.

25672: Tento port je určen pro meziserverovou komunikaci mezi uzly v clusteru RabbitMQ, pokud je váš RabbitMQ nasazen jako cluster.

15671: Stejně jako port 5671, tento port umožňuje zabezpečenou komunikaci pomocí SSL/TLS, ale je určen pro webové rozhraní RabbitMQ Management Plugin.

15672: Toto je port pro RabbitMQ Management Plugin, což je webové rozhraní pro správu RabbitMQ. Pomocí tohoto rozhraní můžete monitorovat a spravovat své RabbitMQ instance.

Do webového rozhraní tedy můžeme nahlédnout přes URL http://localhost:15672

Defaultní uživatelské jméno: guest a heslo: guest
My jsme si změnili uživatelské jméno: user a heslo: password

C# Dynamický datový typ

Objčas potřebujeme udělat dynamicky malý objekt, kterému nastavíme parametry a pošleme ho dál. Ano, je to ošklivé, nemělo by se to dělat ale občas se to prostě hodí 🙂

dynamic flowCalculator = new
{
   Medium = inputData.HeaterEnum.ToString(),
   WaterTempIn = 80,
   WaterTempOut = 60

};

Jednoduše do toho dynamicky vygenerovaného objektu můžeme přidat další propertu:

if (inputData.MediumEnum == Medium.GLYCOL)
{
   flowCalculator.Concentration = inputData.Concentration;
}

System.Text.Json deserializace case insensitive

V C# objektu nazýváme property PascalCase s velkým písmenem na začátku.

    public class StudentDto
    {

        public long Id { get; set; }
        public string FirstName { get; set; }
        public double LastName { get; set; }
    }

Pokud na tento objekt chceme namapovat json, většinou narazíme na problém s rozdnou velikostí písmen a property se z jsonu nenamapují:

json:

{
    "id": 1,
    "firstName": "Jan",
    "lastName": "Novák"

}

Desertializace:

var student = JsonSerializer.Deserialize<Student>(jsonString);

Property v objektu zůstanou prázdné, protože nedošlo k namapování z jsonu (rozdílná velikost písmen)

Deserializace jsonu case insensitive

Serializeru můžeme nastavit propertu aby nerozlišoval velokost písmen:

var options = new JsonSerializerOptions 
{ 
    PropertyNameCaseInsensitive = true 
}; 

var student = JsonSerializer.Deserialize<Student>(jsonString, options);

Inastalace Camunda v kontejneru

Camunda je engine pro procesní řízení.

Business Process Model and Notation (BPMN) – notace pro modelování procesů.

Aktuálně je Camunda ve verzi 7 (comunity) a verzi 8 (enterpise – placená).

Pojďme si ukázat jak ji jednoduše zprovoznit v kontejneru Camunda 7.

docker pull camunda/camunda-bpm-platform:run-latest
docker run -d --name camunda -p 7777:8080 camunda/camunda-bpm-platform:run-latest

Na výsledek se můžeme podívat: http://127.0.0.1:7777/camunda-welcome/index.html

Defaultní přístupové údaje:

  • Username: demo
  • Password: demo

Camunda REST API + SWAGGER

Camunda se skládá z několika modulů

  • web-app – (balíčky: Tasklist, Cockpit a Admin)
  • rest – REST API
  • swaggerui – swagger UI rozhraní – dokumentace k REST API endpointům s možností otestování

Pokud potřebujeme používat REST API spustíme kontejner s požadovanými parametry:

docker pull camunda/camunda-bpm-platform:latest
docker run -d --name camunda -p 7777:8080 camunda/camunda-bpm-platform:latest ./camunda.sh --webapps --rest --swaggerui

APP: http://127.0.0.1:7777/camunda-welcome/index.html
API: http://127.0.0.1:7777/engine-rest/
Swagger: http://127.0.0.1:7777/swaggerui/

Defaultní přístupové údaje:

  • Username: demo
  • Password: demo

Quasar

Inicializace projektu

$ npm i -g @quasar/cli 
$ npm init quasar

Spuštění projektu

cd projek-folder
quasar dev

Nová komponenta

quasar new component <name> [--format ts]

Boot soubor

quasar new boot <name> [--format ts]

Základní příkazy

$ quasar
   ...
   Commands
      init       Create a project folder
      dev       Start a dev server for your App
      build       Build your app for production
      clean       Clean all build artifacts
      new       Quickly scaffold page/layout/component/... vue file
      mode       Add/remove Quasar Modes for your App
      info       Display info about your machine and your App
      serve       Create an ad-hoc (production-ready) server on App distributables
      help       Displays this message

Vue.js 3 – Hello world

npm create vue@3

Budeme vyzváni k zadání:

  • názvu projektu
  • podpory TypeStcriptu
  • podpory JSX
  • testů, ESlintu, …

Po vytvoření kostry projektu stačí nainstalovat balíčky a projekt spustit:

cd vue-project
npm install
npm run dev

Tip

Pokud se při spuštění vyskytnou problémy (failed to load config from vite.config.js, Error: Cannot find module ‚node:url‘, …) aktualizujte node.js na najnovější LTS verzi.

Spuštění projektu

Pokud vše prošlo, uvidíte v konzoli:

VITE v4.0.4 ready in 307 ms

➜ Local: http://localhost:5173/
➜ Network: use –host to expose
➜ press h to show help

Otevřee internetový prohlížeč a zadejte do něj adresu z konzole: http://localhost:5173/

React – Hello world

Vygenerování nové aplikace:

npx create-react-app hello-world

Spustit příkazem:

cd hello-world
npm start

Komponenty

npx generate-react-cli component Box

Do app.js přidat:

import Box from './components/Box/Box';
...
  return (
    <div className="App">
      <header className="App-header">
        <Box></Box>
      ...

npm start
Starts the development server.

npm run build
Bundles the app into static files for production.

npm test
Starts the test runner.

npm run eject
Removes this tool and copies build dependencies, configuration files
and scripts into the app directory. If you do this, you can’t go back!

C# serializace a deserilizace výčtového typu

V C# se při serializaci a deserializaci JSON velmi často potřebujeme vypořádat s výčtovými typy (enum). Ve výchozím nastavení je hodnota z výčtového typu interpretována jako celé číslo – to většinou nestačí, protože požadujeme textovou hodnotu. Pojďme si ukázat, jak to vyřešit nativní knihovnou System.Text.Json a knihovnou Newtonsoft.Json

public class Student
{
    public string Name { get; set; }
    public SexEnum Sex { get; set; }

}

public enum SexEnum 
{
    Female, Male
}

var student1 = new Student() { Name = "Pepek Vyskoc", Sex = SexEnum.Male }

// Native serialize
JsonSerializer.Serialize(student1);

// Newtonsoft
return JsonConvert.SerializeObject(student1);

Vrátí json:

{
  "Name": "Pepek Vyskoc",
  "Sex": 1
}

Funkční ale dost často nedostačující, protože z pohlaví 0 a 1 toho moc nepoznáme.

Serializace enum jako textový řetězec

Stačí přidat k požadované propertě anotaci *EnumConverter:

// Native
public class Student
{
    ...
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public SexEnum Sex { get; set; }
    ...
}
// Newtonsoft
public class Student
{
    ...
    [JsonConverter(typeof(StringEnumConverter))]
    public SexEnum Sex { get; set; }
    ...
}

Kde získáme požadovaný formát odpovědi:

{
  "Name": "Pepek Vyskoc",
  "Sex": "Male"
}

Globální serializace enum jako textový řetězec

Pokud nechcete před každou propertu uvádět anotaci pro converter, máte možnost ji nastavit globálně do souboru Program.cs:

// Native
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
    });

// Newtonsoft
builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.Converters.Add(new StringEnumConverter());
    });

Tím dosáhneme požadovaného výsledku automaticky