.NET Dependency Injection (DI)

Dependency Injection je návrhový vzor, který umožňuje odstranit pevné vazby mezi softwarovými komponentami. Místo přímého vytváření závislých objektů v rámci třídy, DI poskytuje tyto závislosti zvenčí, což vede k větší flexibilitě a oddělení. Ve světě C#, kde aplikace často rostou do velké komplexnosti a musí být snadno testovatelné a rozšiřitelné, je DI klíčovým nástrojem, který vývojářům pomáhá dosáhnout těchto cílů.

Pojďme si ukázat životní cykly DI služeb – jako jsou Singleton, Scoped a Transient.

Singleton

  • Životní cyklus: Instance služby Singleton je vytvořena pouze jednou za dobu existence aplikace (nebo hostitele). To znamená, že všechny požadavky a operace používají stejnou instanci služby.
  • Použití: Singleton je vhodný pro služby, které mají globální stav nebo konfiguraci, která se nemění, nebo pro služby, které vyžadují pouze jednu instanci (například logovací služby, přístup k konfiguračním souborům atd.).
  • Rizika: Je třeba být opatrný při používání stavových služeb Singleton ve vícevláknových aplikacích, protože to může vést k problémům s bezpečností vláken.

Scoped

  • Životní cyklus: Scoped služby jsou vytvořeny jednou pro každý scope, typicky pro každý HTTP požadavek v ASP.NET Core. Každý požadavek má svou vlastní instanci scoped služby.
  • Použití: Scoped služby jsou ideální pro operace, které jsou specifické pro konkrétní požadavek nebo transakci, jako jsou například databázové kontexty v Entity Framework.
  • Rizika: Scoped služby by neměly být sdíleny mezi různými požadavky, protože mohou obsahovat stav specifický pro konkrétní požadavek.

Transient

  • Životní cyklus: Transient služby jsou vytvářeny pokaždé, když jsou požadovány. To znamená, že každé vložení (injection) nebo každý požadavek na službu vytvoří novou instanci této služby.
  • Použití: Jsou ideální pro lehké, bezstavové služby. Protože jsou transient služby krátkodobé, jsou vhodné pro služby, které neuchovávají informace o stavu nebo zdroje mezi jednotlivými požadavky.
  • Rizika: Vytváření nové instance při každém požadavku může být náročné na zdroje, pokud je služba náročná na vytvoření, nebo pokud je často požadována.

Shrnutí:

  • Singleton: Jedna instance pro celou aplikaci.
  • Scoped: Jedna instance na scope (typicky na HTTP požadavek).
  • Transient: Nová instance při každém požadavku na službu.

Při návrhu aplikace je důležité pečlivě zvážit, který životní cyklus je pro každou službu nejvhodnější, aby se dosáhlo správného vyvážení mezi výkonem, bezpečností vláken a správou zdrojů.

.Net core + NUnit testujeme a mockujeme

Mockování je důležitým konceptem při psaní jednotkových testů z několika důvodů:

  1. Izolace od závislostí: Mockování umožňuje oddělit testovanou třídu od jejích závislostí. To znamená, že při testování konkrétní třídy nemusíte brát v potaz implementaci jejích závislostí. Místo toho můžete vytvořit mock objekty, které simulují chování těchto závislostí.
  2. Opakovatelnost a konzistence: Při použití skutečných závislostí, jako jsou databáze nebo externí API, mohou testy být nekonzistentní a záviset na stavu těchto závislostí. Použití mocků umožňuje vytvořit konzistentní a opakovatelné testy, které nejsou závislé na externích faktorech.
  3. Rychlost testů: Skutečné závislosti mohou být pomalé nebo nedostupné v určitých situacích, což může zpomalit běh testů. Použití mocků umožňuje rychlejší spouštění testů, protože mocky jsou obvykle rychlejší než skutečné závislosti.
  4. Izolace chyb: Pokud test selže, mocky vám umožňují izolovat problém na konkrétní část kódu, kterou testujete. Bez mocků by selhání mohlo být způsobeno problémem v externí závislosti, což by mohlo být složité odhalit.
  5. Testování hraničních podmínek a chybových stavů: Mocky umožňují snadno simulovat různé situace, včetně chybových stavů, které mohou být obtížné dosáhnout s reálnými závislostmi.
  6. Zvýšení rychlosti vývoje: Testy s mocky umožňují rychlejší iterace při vývoji. Můžete psát testy pro nový kód, i když jeho závislosti nejsou ještě implementovány.
  7. Snížení nákladů na infrastrukturu: Pokud byste všechny testy spouštěli s reálnými závislostmi, mohlo by to vyžadovat drahou infrastrukturu, například databázový server. Mockování umožňuje testování bez skutečné infrastruktury.

Celkově mockování je důležitou technikou pro vytváření izolovaných a efektivních jednotkových testů, které vám pomáhají zajistit kvalitu kódu a snižovat rizika chyb.

Instalujeme Moq

Abychom mohli začít mockovat doinstalujeme Nuget do projektu s testy:

Install-Package Moq

Kód pro otestování

Mějme controller a service, které chceme otestovat. Service se do controlleri injectne přes DI:

ProductController.cs:

namespace WebAPiDi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class TestController : ControllerBase
    {
        readonly IProductService _productService;

        public TestController(IProductService productService)
        {
            _productService = productService;
        }

        [HttpGet]
        public IActionResult Shop()
        {
            return Ok(_productService.Shop());
        }
    }
}

ProductService.cs:

namespace WebAPiDi.Services
{
    public class ProductService : IProductService
    {
        public IEnumerable<ProductDto> Shop()
        {
            return new List<ProductDto>()
            {
                new ProductDto() { Id = 1, Name = "Best product" }
            };
        }
    }
}

IProductService.cs:

namespace WebAPiDi.Services
{
    public interface IProductService
    {
        IEnumerable<ProductDto> Shop();
    }
}

productDto.cs:

namespace WebAPiDi.Dtos
{
    public class ProductDto
    {
        public int Id { get; set; }

        public required string Name { get; set; }
    }
}

Program.cs (konfigurace sávislostí):

builder.Services.AddScoped<IProductService, ProductService>();

Testujeme

V minulých dílech jsme si ukázali jak zprovoznit NUnit a Moq. Dnes už je jdeme rovnou používat.

Testujeme service

Začneme tím jednodušším – otestujme ProducService. Nepotřebujeme nic mockovat, pouze si uděláme instanci ProuctService a zavoláním metody Shop zkontrolujeme že vrátí 1 záznam, ve kterém zkontrolujeme vyplnění propert:

namespace Test.Services
{
    [TestFixture]
    public class ProductServiceTests
    {
        [Test]
        public void Shop_ReturnsListOfProducts()
        {
            // Arrange
            IProductService productService = new ProductService();

            // Act
            IEnumerable<ProductDto> products = productService.Shop();

            // Assert
            Assert.IsNotNull(products);
            Assert.IsInstanceOf<IEnumerable<ProductDto>>(products);
            Assert.That(products.Count(), Is.EqualTo(1));

            foreach (var product in products)
            {
                Assert.IsNotNull(product.Id);
                Assert.IsNotNull(product.Name);
            }
        }
    }
}

Při testování controlleru budeme muset použít mockování. Namockujeme se ProductService, čímž vytvoříme izovali od této service a můžeme si nasimulovat nejrůznější chování této service.

namespace Test.Controllers
{
    [TestFixture]
    public class TestControllerTests
    {
        [Test]
        public void Shop_ReturnsOkResultWithProducts()
        {
            // Arrange
            var productServiceMock = new Mock<IProductService>();
            productServiceMock.Setup(repo => repo.Shop()).Returns(new List<ProductDto>
            {
                new ProductDto() { Id = 68, Name = "Jarda Jagr" },
                new ProductDto() { Id = 88, Name = "pasta" }
            });

            var controller = new TestController(productServiceMock.Object);

            // Act
            var result = controller.Shop() as OkObjectResult;

            // Assert
            Assert.IsNotNull(result);
            Assert.That(result.StatusCode, Is.EqualTo(200));

            var products = result.Value as List<ProductDto>;
            Assert.IsNotNull(products);
            Assert.That(products.Count, Is.EqualTo(2));
        }

        [Test]
        public void Shop_ReturnsEmptyProducts()
        {
            // Arrange
            var productServiceMock = new Mock<IProductService>();
            productServiceMock.Setup(repo => repo.Shop()).Returns(new List<ProductDto>());

            var controller = new TestController(productServiceMock.Object);

            // Act
            var result = controller.Shop() as OkObjectResult;

            // Assert
            Assert.IsNotNull(result);
            Assert.That(result.StatusCode, Is.EqualTo(200));

            var products = result.Value as List<ProductDto>;
            Assert.IsNotNull(products);
            Assert.That(products.Count, Is.EqualTo(0));
        }
    }
}

.Net core + NUnit testujeme statickou třídu

Testování statických tříd je .Net velice jednoduché. Nemusíte nic mockovat a jednoduše napíšete test. NUnit umožňuje testovat statické třídy stejně jako běžné třídy, ale nemáte k dispozici instanci třídy, takže nemůžete testovat instanční metody ani vlastnosti. Místo toho se zaměříme na testování statických metod a vlastností.

public static class MathUtility
{
    public static int Add(int a, int b)
    {
        return a + b;
    }

    public static int Subtract(int a, int b)
    {
        return a - b;
    }
}

Ve Visual Studiu založíme ve stejné solution nový projekt, který bude vycházet ze šablony Nunit Test Project. Do toho projektu založíme nový test:

namespace Test.Statics
{
    [TestFixture]
    public class MathUtilityTests
    {
        [Test]
        public void Add_ShouldReturnCorrectSum()
        {
            int result = MathUtility.Add(3, 5);
            Assert.AreEqual(8, result);
        }

        [Test]
        public void Subtract_ShouldReturnCorrectDifference()
        {
            int result = MathUtility.Subtract(10, 3);
            Assert.AreEqual(7, result);
        }
    }
}

Po spuštění testu (na testovacím projektu klikneme pravým talčítkem a vyberme volbu Run tests) vidíme výsledky testů

Jak funguje Assert.AreEqual

Metoda Assert.AreEqual(8, result) v NUnit je způsob, jak otestovat, zda očekávaná hodnota (v tomto případě 8) je rovna aktuální hodnotě (v tomto případě result). Pokud jsou tyto dvě hodnoty shodné, test projde a pokračuje v běhu testů. Pokud jsou hodnoty různé, test selže a testovací běh je přerušen.

Zde je, jak to funguje:

  • Pokud result (aktuální hodnota) a 8 (očekávaná hodnota) jsou stejné, test projde a není vyvolána výjimka.
  • Pokud result a 8 nejsou stejné, test selže a vyvolá se výjimka AssertionException, která způsobí, že testovací běh se zastaví a označí test jako nevyhovující.

Tato metoda je používána k porovnání dvou hodnot a určení, zda jsou shodné. Použití této metody v testech je běžným způsobem ověření správného chování kódu. V tomto konkrétním případě se testuje, zda výstup z metody MathUtility.Add(3, 5) je roven 8.

Poznámka: Můžete také použít jiné metody Assert, jako například Assert.IsTrue, Assert.IsFalse, nebo Assert.IsNull, atd., v závislosti na tom, co chcete otestovat.

Vylepšení

Můžete si všimnout, že Visual Studio řádek s ověřením výsledku podtrhne, protože se mu na něm něco nelíbí:

Assert.AreEqual(8, result);

U řádku vidíme varování: Consider using the constraint model, Assert.That(actual, Is.EqualTo(expected)), instead of the classic model, Assert.AreEqual(expected, actual).

Více o problému se dočete zde: https://docs.nunit.org/articles/nunit-analyzers/NUnit2005.html

Pokud se chcete toto varování opravit, přepište ověření výsledku na:

Assert.That(result, Is.EqualTo(7));

Python – monitorujeme aktuální spotřebu přes chytrou zásuvku P110

Zásuvka Tapo P110 je jedním z produktů od společnosti TP-Link, který umožňuje smart ovládání přes WiFi. Jedna z výhod této zásuvky je, Dnes se podíváme na tuto chytrou zásuvku, kterou budeme na dálku ovládat a měřit spotřebu. Jednou z výhod této zásuvky oproti nižšímu modelu P100 je monitorování spotřeby elektrické energie. To nám umožní sledovat kolik elektřiny jednotlivá zařízení spotřebovávají.

Dnešní řešení provedeme v programovací m jazyce Python s využitím knihovny PyP100. Nenechte se zmást názvem knihovny PyP100, tato knihovna umí pracovat i se zásvkou P110.

Neprve zapojte zásuvku do elektřiny, přidejte si ji do vaší domácnosti a zprozněte v aplikaci TAPO.

Zásuvka komunikuje s okolím přes WiFi, tekže zařízení kterým budete zásvuku ovládat / monitorovat odběr přes zásuvku musí být ve stejné síti. V mém případě se jedná o Raspberry.

V apliakci je pořeba zjistit IP adresu zásvuky. Provedete to v detailu zásuvky -> Info o zařízení -> IP adresa.

Instalace Python na Raspberry Pi

Na Raspberry Pi mám Ubuntu:

sudo apt update 
sudo apt upgrade
sudo apt install python3
python3 --version

Programujeme

Použijeme Python s knihovnu https://pypi.org/project/PyP100/

sudo pip install PyP100

V případě chyby:

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.

sudo pip install PyP100 --break-system-packages

Pak už jen napíšeme kód pro ovládání zásvuky. Nezapomeňte, že zásuvku musíte mít přidanou ve va3i aplikaci, protože pro ovládání zásuvky bude potřeba jméno a heslo k vašemu účtu TAPO.

from PyP100 import PyP110

p110 = PyP100.P110("192.168.X.X", "email@email.cz", "heslo") #IP adresa zasuvky, email, heslo

p110.handshake() #cookies pozadavek
p110.login()


p110.getEnergyUsage() #aktualni spotreba zasuvky

#sprace se stavy zasuvky
p110.turnOn() #Zapne zasuvku
p110.turnOff() #Vypne zasuvku
p110.toggleState() #zmeni stav zasuvky na opacny zapne/vypne

p110.turnOnWithDelay(10) #zapne se zpozdenim 10s
p110.turnOffWithDelay(10) #vypne se zpozdenim 10s

p110.getDeviceInfo() #informace o zasuvce
p110.getDeviceName() #jmeno zasuvky

Ubuntu – jak spustit script po startu systému – služba

Pro spuštění skriptu po startu systému na Raspberry Pi s operačním systémem Ubuntu, můžete využít službu systému systemd. systemd je systémový a službový správce většiny moderních linuxových distribucí, včetně Ubuntu.

  1. Vytvořte skript, který chcete spustit po startu systému. Scriptu musíte nastavit oprávnění pro spuštění. Například, pokud máte skript ble-advertisements.py, ujistěte se, že má správná oprávnění pro spuštění: chmod +x ble-advertisements.sh.
  2. Vytvořte systemd službu. Vytvořte nový soubor s příponou .service ve složce /etc/systemd/system/. Můžete to provést například pomocí editoru vim:
sudo vim /etc/systemd/system/ble-advertisements.service

V tomto souboru jsou informace o službě:

[Unit]
Description=BLE advertisements

[Service]
ExecStart=/home/ubuntu/scripts/ble-advertisements.sh
WorkingDirectory=/home/ubuntu/scripts
Restart=always
User=ubuntu

[Install]
WantedBy=multi-user.target

Souboru nastave oprvávnění pro spuštění:

sudo chmod ugo+x /etc/systemd/system/ble-advertisements.service

Upravte následující informace podle vašich potřeb:

  • Description – Popis vaší služby.
  • ExecStart – Cesta k vašemu skriptu, který chcete spustit.
  • WorkingDirectory – Volitelně, cesta k pracovní složce, kde se skript spouští.
  • User – Uživatel, pod kterým chcete spustit skript. Zde je uveden uživatel „ubuntu“, ale upravte jej podle vašich potřeb.

Aktualizujte systemd a povolte vaši službu:

sudo systemctl daemon-reload
sudo systemctl enable ble-advertisements.service

Restartujte zařízení. Po restartu se služba automaticky spustí:

sudo reboot

Sledování statvu služby

systemctl status ble-advertisements.service

journalctl -u ble-advertisements.service

Změny stavu služby

systemctl restart ble-advertisements.service
systemctl stop ble-advertisements.service
systemctl start ble-advertisements.service

Entity Framework Core – logování selectů a doby vykonávání

Časem narazíte na problémy pomalých odezev vašich aplikací. Budete chtít mít představu kdy a jak dlouho co trvalo – kdo neměří ten neřídí. Proto si dnes ukážeme jak logovat vygenerované SQL dotazy včetně jejich časi provedení. Entity Framework defaultně loguje příkazy, které vykonává, ovšem to dost často nestačí. Dnes si ukážeme jak logování rozšířit.

Zabudovaného logování v EF Core

Jedná se o základní jedochudou metodu. V Program.cs máte kód který přidává EF do DI:

//EF
builder.Services.AddDbContext<HulvatContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection");
});

Tento kód stačí rozšířit o požadované logování:

//EF
builder.Services.AddDbContext<HulvatContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
    .UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()))
    .EnableSensitiveDataLogging();
});

Tím získáme SQL dotaz včetně doby vykonávání:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (10ms) [Parameters=[@__email_0='test@test.cz' (Size = 50)], CommandType='Text', CommandTimeout='30']
SELECT TOP(1) [u].[Id], [u].[DisplayName], [u].[Email], [u].[FisrtName], [u].[LastName] FROM [Users] AS [u]
WHERE [u].[Email] = @__email_0

Tímto způsobem budou SQL dotazy logovány do konzole. Můžete změnit cíl logování tím, že nahradíte AddConsole() za jiný logger.

EnableSensitiveDataLogging

Při logování SQL dotazů s citlivými daty (sensitive data), můžete použít metodu .EnableSensitiveDataLogging(). Tato metoda zajistí, že citlivá data, jako jsou hesla, nebudou logována.

.NET core – jak logovat s NLog

Nes si ukážeme jak zprovoznit logování přes knihovnu NLog. V této knihovně je možné udělat několik targertů a do každého targetu logovat různé informace. Například pouze chyby logovat do speciálního targetu a posílat si je například emailem, …

Instalace NLog

Install-Package NLog
Install-Package NLog.Web.AspNetCore

Konfigurace NLog

Vytvoříme soubor konfigurace pro NLog, který určuje, kam a jak mají být logy zpracovány. Vytvořte soubor s názvem „nlog.config“ ve vašem projektu (na úrovni souboru appsettings.json):

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  autoReload="true"
  internalLogLevel="info"
  internalLogFile="c:\\_temp\\internallog.txt">
  <extensions>
    <add assembly="NLog.Web.AspNetCore" />
  </extensions>
  <targets>
    <target name="logfile" xsi:type="File" fileName="c:\\_temp\\log.txt" layout="${longdate} ${level:uppercase=true} ${message}"/>
     <target xsi:type="ColoredConsole" name="logconsole" />
  </targets>
  <rules>
    <logger name="Microsoft.*" maxLevel="Info" final="true" />
    <logger name="*" minlevel="Info" writeTo="logfile" />
    <logger name="*" minlevel="Info" writeTo="logconsole" />
  </rules>
</nlog>

Tento konfigurační soubor nastaví logování do souboru „log.txt“ od úrovně „Info“ výše a zahrnuje všechny loggery (názvy hvězdičkou). Můžete dále upravit konfiguraci podle vašich potřeb. Osvědčilo se mi posálání chyb emailem.

Registrujeme NLog v aplikaci

Do souboru Program.cs přidáme:

var logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();

//Nlog
builder.Logging.ClearProviders();
builder.Host.UseNLog();

Logujeme v aplikaci

Nyní máme 2 možnosti logování

Logování přes GetCurrentClassLogger

Definujeme a rovnou naplníme field logeru:

private static Logger _logger = LogManager.GetCurrentClassLogger();

který pak používáme:

_logger.Trace("Trace");
_logger.Debug("Debug");
_logger.Info("Info");
_logger.Warn("Warn");
_logger.Error("Error");

DI ILogger

Přes DI si injecteme logger:

private readonly ILogger _logger;

public UserService(ILogger<UserService> logger)
{

  _logger = logger;
}

Který pak používáme

_logger.LogTrace("Trace");
_logger.LogDebug("Debug");
_logger.LogInformation("Info");
_logger.LogWarning("Warn");
_logger.LogError("Error");

Entity framework – DI

V minulém díle jsme si ukázali jak rozject EF core nad MySQL databází – DB first dnes navážeme trochu obecne. Níže uvedené tipy je možno pužít pro libovolnou DB při přístupu DB first.

DBContext – odebrání connection stringu

Při defaultním generování database first je ve vygenerovaném DbContext souboru uložen connection string pro připojení do DB včetně citlivých údajů:

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
        => optionsBuilder.UseMySql("server=localhost;user=root;database=app_cz", Microsoft.EntityFrameworkCore.ServerVersion.Parse("5.7.17-mysql"));

Do zdrojového kódu nepatří žádné přístupové údaje, ty patří do konfiguračního souboru (appsettings.json) aby je bylo jednoduché kdykoliv změnit. Celou metodu protected override void OnConfiguring uvedenou výše tedy můžeme odstranit.

Do Program.cs přidáme:

//EF
// Replace with your server version and type.
// Use 'MariaDbServerVersion' for MariaDB.
// Alternatively, use 'ServerVersion.AutoDetect(connectionString)'.
// For common usages, see pull request #1233.
var serverVersion = new MySqlServerVersion(new Version(8, 0, 29));

// Replace 'YourDbContext' with the name of your own DbContext derived class.
builder.Services.AddDbContext<ComameEuContext>(
    dbContextOptions => dbContextOptions
        .UseMySql(builder.Configuration["ConnectionStrings:DefaultConnection"], serverVersion)
        // The following three options help with debugging, but should
        // be changed or removed for production.
        .LogTo(Console.WriteLine, LogLevel.Information)
        .EnableSensitiveDataLogging()
        .EnableDetailedErrors());

Pro případný Microsoft SQL Server je konfigurace trochu elegantnější:

//EF
builder.Services.AddDbContext(options =>
{
   options.UseSqlServer(builder.Configuration["ConnectionStrings:DefaultConnection"]);
});

A connectionstring uložíme do konfiguračního souboru appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "server=localhost;user=root;database=app_cz"
  },
  "Logging": {
...

DI pro použití DbContext

Nyní zbývá dořešit poslední problém z minulé ukázky. DbContext je nejlepší injectount na požadované místo použití.

public class MyService
{
    private readonly AppCzContext _dbContext;

    public MyService(AppCzContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IEnumerable<User> GetUsers()
    {
        return _dbContext.Users.ToList();
    }
}

Asynchronní dotazy

Další a dnešní poslední tip je pro používání asynchronních dotazů. Asynchronními dotazy zvyšujete výkon a odezvu vaší aplikace, efektivněji využíváte prostředky a minimalizujete blokování vláken. Takže příště už jen takto:

public async Task<IEnumerable<User>> GetUsersAsync()
{
    return await _dbContext.Users.ToListAsync();
}

.NET Core – malé znaky v URL

Defaultně .NET Core pro web a api generuje URL ve tvaru PascalCase:

    public class HomeController : Controller
    {
        public IActionResult Privacy()
        {
            return View();
        }

    }

Vygneneruje URL ve formě: „/Home/PrivacyAndPolicy“. URL adresa je case insensitive, což znamená, že nezáleží na velikosti písme a kdokoliv kdo bude přistupovat na „/home/privacy“ nebo „/HOME/PRIVACY“ obdrží shodnou odpověď.

Konzistentní malá písmena v URL

Pokud chcete držet stejnou konzistenci a všechny písmena v URL mít malá (jen v URL na parametry nemá vliv 🙂 ) a zároveň posílit SEO, štěte dál. V souboru Program.cs stačí použít:

builder.Services.AddRouting(options => options.LowercaseUrls = true);

camelCase v URL API Swaggeru

Výše uvedený tip má vliv i na výstup ve Swaggeru a v celé aplikaci budou konzistentně malá písman v URL. Občas potřebujete pouze v API použít camelCase pro lepší čitelnost. Pro tento případ stačí použít zápis níže. Tento způsob má výhodu v tom, že tím nic nemůžete rozbít a způsobit breaking change:

services.AddSwaggerGen(c =>
{
    // Další nastavení konfigurace Swagger
    c.DescribeAllParametersInCamelCase = true;
});

.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.