V moderním vývoji softwaru je často nezbytné načítat a vykonávat kód dynamicky za běhu aplikace. Tento přístup nabízí velkou flexibilitu, umožňuje načítat a spouštět funkce nebo třídy, které nebyly známé v době kompilace. Taková technika je velmi užitečná například pro vytváření pluginových systémů nebo aplikací, které musí reagovat na různé požadavky uživatelů bez nutnosti opětovné kompilace. Tento proces může výrazně zlepšit efektivitu správy paměti a umožnit lepší využití zdrojů.

V následujícím textu se podíváme na praktické příklady, jak dynamicky načítat knihovny a provádět metody během běhu aplikace v .NET. Začneme s jednoduchým příkladem, kde upravíme kód tak, aby využíval zastaralé metody a následně je nahrazoval novějšími variantami.

Prvním krokem je vytvoření metody, která označí starou metodu jako zastaralou. V souboru Animal.cs přidáme metodu Speak(), která bude označena atributem Obsolete. Tento atribut upozorňuje na to, že metoda je zastaralá a místo ní by se měla použít nová metoda, například SpeakBetter():

csharp
[Obsolete($"use {nameof(SpeakBetter)} instead.")] public void Speak() { WriteLine("Woof..."); } public void SpeakBetter() { WriteLine("Wooooooooof..."); }

Následně je nutné upravit kód v souboru Program.cs, aby dynamicky detekoval zastaralé metody a výstupem je informace o tom, která metoda byla označena jako zastaralá. Můžeme použít následující kód, který prochází všechny metody a vrací, zda jsou označeny jako zastaralé:

csharp
foreach (MemberInfo member in members) { ObsoleteAttribute? obsolete = member.GetCustomAttribute<ObsoleteAttribute>(); WriteLine("{0}: {1} ({2}) {3}", member.MemberType, member.Name, member.DeclaringType?.Name,
obsolete is null ? "" : $"Obsolete! {obsolete.Message}");
}

Po spuštění programu bude výstup obsahovat informace o metodě Speak(), která bude označena jako zastaralá s uvedením, jak ji nahradit novější metodou SpeakBetter().

Další zajímavou technikou v .NET je dynamické načítání sestav během běhu aplikace. Tento přístup je zvláště užitečný, pokud nevíte, které knihovny budete potřebovat, dokud aplikace není spuštěna. Můžeme mít například aplikaci, která nemusí mít vždy načtené všechny funkce, ale na základě potřeb uživatele může aktivovat konkrétní funkce (například funkci pro hromadnou korespondenci v textovém editoru).

Představme si, že máme projekt DynamicLoadAndExecute.Library, který obsahuje třídu Dog se metodou Speak(). Tato třída bude dynamicky načítána a její metody volány až během běhu aplikace.

Postupujeme podle následujících kroků:

  1. Vytvoříme nový projekt DynamicLoadAndExecute.Library a v něm definujeme třídu Dog:

    csharp
    namespace DynamicLoadAndExecute.Library
    { public class Dog { public void Speak(string? name) { WriteLine($"{name} says Woof!"); } } }
  2. Vytvoříme nový konzolový projekt DynamicLoadAndExecute.Console, který bude načítat knihovnu z prvního projektu a spouštět její metody.

  3. Po vytvoření obou projektů přeneseme potřebné soubory z knihovny do konzolového projektu. Poté definujeme metodu pro výstup informací o načtené sestavě a jejích typech:

    csharp
    partial class Program
    { static void OutputAssemblyInfo(Assembly a) { WriteLine("FullName: {0}", a.FullName); WriteLine("Location: {0}", Path.GetDirectoryName(a.Location)); WriteLine("IsCollectible: {0}", a.IsCollectible); WriteLine("Defined types:"); foreach (TypeInfo info in a.DefinedTypes) { if (!info.Name.EndsWith("Attribute")) { WriteLine(" Name: {0}, Members: {1}", info.Name, info.GetMembers().Count()); } } WriteLine(); } }
  4. Vytvoříme třídu DemoAssemblyLoadContext, která bude zodpovědná za načítání sestav do paměti:

    csharp
    internal class DemoAssemblyLoadContext : AssemblyLoadContext
    { private AssemblyDependencyResolver _resolver; public DemoAssemblyLoadContext(string mainAssemblyToLoadPath) : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath); } }
  5. V souboru Program.cs použijeme tento kontext k dynamickému načítání sestavy a instanciování třídy Dog:

    csharp
    Assembly? thisAssembly = Assembly.GetEntryAssembly();
    if (thisAssembly is null) { WriteLine("Could not get the entry assembly."); return; } OutputAssemblyInfo(thisAssembly); DemoAssemblyLoadContext loadContext = new(thisAssembly.Location); string assemblyPath = Path.Combine(Path.GetDirectoryName(thisAssembly.Location) ?? "", "DynamicLoadAndExecute.Library.dll"); WriteLine("Loading: {0}", Path.GetFileName(assemblyPath)); Assembly dogAssembly = loadContext.LoadFromAssemblyPath(assemblyPath); OutputAssemblyInfo(dogAssembly); Type? dogType = dogAssembly.GetType("DynamicLoadAndExecute.Library.Dog"); if (dogType is null) { WriteLine("Could not get the Dog type."); return; } MethodInfo? method = dogType.GetMethod("Speak"); if (method != null) { object? dog = Activator.CreateInstance(dogType); for (int i = 0; i < 10; i++) { method.Invoke(dog, new object[] { "Fido" }); } } WriteLine(); WriteLine("Unloading context and assemblies."); loadContext.Unload();

Tento postup umožňuje dynamické načítání knihoven a jejich metod za běhu aplikace. Výhoda tohoto přístupu spočívá nejen ve flexibilitě a efektivitě, ale také v optimalizaci využití paměti, protože knihovny lze načítat pouze v případě potřeby a po jejich použití je lze z paměti uvolnit.

Důležité aspekty k pochopení:

  • Dynamické načítání sestav je užitečné pro aplikace, které potřebují pracovat s různými moduly nebo pluginy, které nejsou známé předem.

  • Použití reflexe pro invokaci metod může mít výkonnostní náklady, ale moderní verze .NET, jako .NET 7, tento proces výrazně optimalizují.

  • Správa paměti při dynamickém načítání sestav je také klíčová, protože nesprávné uvolnění zdrojů může vést k únikům paměti. Proto je důležité používat AssemblyLoadContext pro správné uvolnění knihoven po jejich použití.

Jak implementovat rate limiting a autentifikaci v .NET Web API

V moderních webových aplikacích je správné řízení přístupu a ochrana před přetížením klíčovými aspekty, které zajišťují stabilitu a bezpečnost. Mezi efektivní techniky, jak tyto problémy řešit, patří omezení počtu požadavků (rate limiting) a autentifikace pomocí tokenů. V této kapitole se zaměříme na to, jak implementovat tyto mechanismy v aplikaci založené na .NET Web API.

Rate limiting je technika, která omezuje počet požadavků, které mohou být provedeny během určitého časového okna. Tento mechanismus je nezbytný pro ochranu aplikace před zneužitím, například při masovém odesílání požadavků nebo při útocích typu DoS (Denial of Service). V .NET aplikacích lze rate limiting implementovat jednoduše díky vestavěné podpoře, která byla představena v ASP.NET Core.

Pro implementaci rate limiting v projektu Web API začneme importováním potřebných knihoven, jak ukazuje následující kód:

csharp
using Microsoft.AspNetCore.RateLimiting; using System.Threading.RateLimiting;

Dále nastavíme proměnnou, která bude určovat, zda chceme používat tuto vestavěnou funkci:

csharp
bool useMicrosoftRateLimiting = true;

V následujícím kroku je třeba nakonfigurovat endpoint pro získávání produktů. Tento endpoint bude omezen pravidlem, které povolí maximálně 5 požadavků za každých 10 sekund:

csharp
app.MapGet("api/products", ( [FromServices] NorthwindContext db, [FromQuery] int? page) => db.Products.Where(product => (product.UnitsInStock > 0) && (!product.Discontinued)) .Skip(((page ?? 1) - 1) * pageSize) .Take(pageSize) ) .WithName("GetProducts") .RequireRateLimiting("fixed5per10seconds");

Na konci souboru Program.cs musíme přidat konfiguraci pro rate limiting, kde definujeme konkrétní pravidlo omezující počet požadavků:

csharp
if (useMicrosoftRateLimiting) {
RateLimiterOptions rateLimiterOptions = new(); rateLimiterOptions.AddFixedWindowLimiter( policyName: "fixed5per10seconds", options => { options.PermitLimit = 5; options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; options.QueueLimit = 2; options.Window = TimeSpan.FromSeconds(10); }); app.UseRateLimiter(rateLimiterOptions); }

Po úspěšném nastavení rate limiting můžete testovat aplikaci. V konzolovém klientovi budou požadavky podléhat těmto limitům. Například, pokud uživatel provede více než 5 požadavků za 10 sekund, aplikace pozastaví požadavky, dokud nevyprší časové okno.

Další důležitou součástí bezpečnosti webových služeb je autentifikace a autorizace. V tomto případě je jedním z nejběžnějších způsobů autentifikace použití JWT (JSON Web Token). JWT je standard pro bezpečnou výměnu informací mezi stranami ve formátu JSON. Tento token je podepsán, což zaručuje jeho pravost. Autentifikace pomocí JWT je často používána pro ověřování identity uživatelů při přístupu k chráněným API.

V rámci vývoje aplikace musíme přidat podporu pro JWT autentifikaci. Nejprve je nutné importovat příslušný namespace pro práci s bezpečnostními nároky:

csharp
using System.Security.Claims;

Pak přidáme potřebné služby pro autentifikaci a autorizaci v souboru Program.cs:

csharp
builder.Services.AddAuthorization(); builder.Services.AddAuthentication(defaultScheme: "Bearer") .AddJwtBearer();

Na závěr je třeba zajistit, že aplikace bude používat autentifikaci a autorizaci při každém požadavku:

csharp
var app = builder.Build();
app.UseAuthorization();

Díky těmto nastavením může webová aplikace ověřovat požadavky na základě JWT tokenů, které jsou připojeny k požadavkům uživatelů. Tokeny jsou generovány po úspěšné autentifikaci uživatele a slouží k ověření jeho identity při každém dalším přístupu k chráněným zdrojům.

Přístup k citlivým datům by měl být vždy chráněn odpovídajícími bezpečnostními mechanismy. Ačkoli rate limiting a autentifikace pomocí JWT představují základní bezpečnostní vrstvy, je důležité nezapomínat na další kroky, jako je validace vstupů, šifrování dat v průběhu přenosu (například pomocí HTTPS) a pravidelné aktualizace softwaru. Celkově jde o to, aby aplikace byla schopná efektivně řídit přístup a chránit se před zneužitím.

Jak efektivně využít OData pro webové aplikace v prostředí ASP.NET MVC

Vytváření webových aplikací s využitím OData poskytuje efektivní způsob, jak zpřístupnit data pro různé klienty. Tento proces se neomezuje pouze na jednoduchý přístup k datům, ale umožňuje také jejich filtrování, třídění a rozšiřování o další související informace. V této části se zaměříme na integraci OData do MVC aplikace pomocí konkrétního příkladu práce s daty o produktech.

Začneme tím, že ve Visual Studio Code vybereme projekt Northwind.OData.Client.Mvc jako aktivní projekt OmniSharp. Následně přidáme referenci na projekt Northwind.Common.EntityModels.SqlServer, který je umístěn ve složce Chapter02. Po sestavení tohoto projektu otevřeme soubor launchSettings.json ve složce Properties a upravíme port pro HTTPS na 5102 tak, jak je uvedeno v následujícím kódu:

json
"applicationUrl": "https://localhost:5102"

Dále v projektu Northwind.OData.Client.Mvc ve složce Models vytvoříme novou třídu ODataProducts.cs, která bude definovat strukturu pro produkty vrácené službou OData. Tato třída bude obsahovat pole Value, které bude polem objektů typu Product:

csharp
using Packt.Shared; namespace Northwind.OData.Client.Mvc.Models; public class ODataProducts { public Product[]? Value { get; set; } }

V souboru Program.cs přidáme kód pro nastavení typu médií v HTTP hlavičce:

csharp
using System.Net.Http.Headers;

Následně registrujeme HTTP klienta pro službu OData, který bude používat formát JSON pro odpověď:

csharp
builder.Services.AddHttpClient(name: "Northwind.OData", configureClient: options => { options.BaseAddress = new Uri("https://localhost:5101/");
options.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json", 1.0));
});

Nyní přistoupíme k volání služby OData na domovské stránce aplikace. V souboru HomeController.cs deklarujeme pole pro uchování zaregistrované HTTP klientské služby. Dále v konstruktoru třídy přidáme kód pro její předání a uložení:

csharp
public HomeController(ILogger logger, IHttpClientFactory clientFactory) { _logger = logger; this.clientFactory = clientFactory; }

Metodu Index učiníme asynchronní a přidáme kód pro volání služby OData, která vrací produkty začínající písmeny "Cha". Výsledek uložíme do slovníku ViewData, aby ho bylo možné zobrazit v pohledu:

csharp
public async Task Index(string startsWith = "Cha") {
try { HttpClient client = clientFactory.CreateClient(name: "Northwind.OData"); HttpRequestMessage request = new(HttpMethod.Get, "catalog/products/?$filter=startswith(ProductName, '" + startsWith + "')&$select=ProductId,ProductName,UnitPrice"); HttpResponseMessage response = await client.SendAsync(request); ViewData["startsWith"] = startsWith; ViewData["products"] = (await response.Content.ReadFromJsonAsync<ODataProducts>())?.Value; } catch (Exception ex) { _logger.LogWarning($"Northwind.OData service exception: {ex.Message}"); } return View(); }

Na straně zobrazení (Index.cshtml) odstraníme původní markup a přidáme nový kód pro vykreslení produktů a formulář pro zadání začátku názvu produktu:

csharp
@using Packt.Shared
@{ ViewData["Title"] = "Home Page"; Product[]? products = ViewData["products"] as Product[]; } @ViewData["Title"] @if (products is not null) { <h2>Products that start with '@ViewData["startsWith"]' using OData</h2> @if (products.Length == 0) { <p>No products found.</p> } else { @foreach (Product p in products) { <div> @p.ProductId @p.ProductName @(p.UnitPrice is null ? "" : p.UnitPrice.Value.ToString("c")) </div> } } } <form method="get" action="">
<input type="text" name="startsWith" value="@ViewData["startsWith"]" />
<button type=
"submit">Search</button> </form>

Po spuštění služby OData a MVC aplikace si prohlédneme výsledky v prohlížeči na adrese https://localhost:5102/. V tomto případě by se měly zobrazit tři produkty, jejichž názvy začínají písmeny "Cha". Pokud změníme hodnotu v textovém poli a stiskneme Enter, aplikace vrátí pouze produkty, které začínají novým písmenem.

Dále můžeme otestovat komplexní dotaz na službu OData. Vytvoříme soubor odata-final-query.http a pomocí nástroje RestClient otestujeme následující dotaz:

http
GET https://localhost:5101/catalog/products?$filter=contains(ProductName, 'ch') and UnitPrice lt 44.95 &$orderby=Supplier/Country,UnitPrice &$select=ProductName,UnitPrice &$expand=Supplier

Výsledek tohoto dotazu bude obsahovat produkty, jejichž název obsahuje řetězec "ch" a cena je nižší než 44,95, seřazené podle dodavatele a ceny.

Důležité je si uvědomit, že OData umožňuje velmi flexibilní práci s daty na serveru a minimalizuje objem přenášených dat mezi serverem a klientem. Filtrování a třídění dat na serveru může výrazně zvýšit výkon aplikace, protože se minimalizuje potřeba zpracovávat velké objemy dat na straně klienta. Uživatelé by měli také chápat, jak správně definovat požadavky na data v URL (pomocí různých parametrů OData), což je klíčové pro správnou interakci s API.

Jak využít GraphQL pro kombinování dat z více zdrojů

V oblasti moderní webové vývoje se stále častěji setkáváme s potřebou integrace různých datových zdrojů do jedné aplikace. Jedním z efektivních nástrojů pro tuto úlohu je GraphQL, který umožňuje flexibilní dotazování a manipulaci s daty. Tento přístup se ukáže jako obzvlášť užitečný při práci s více datovými zdroji, jak ukazuje následující příklad integrace GraphQL do aplikace .NET.

Ve složce Models jsme vytvořili tři nové třídy, které nám pomáhají s organizací dat, která budeme potřebovat pro naše uživatelské rozhraní. Začneme definováním třídy ResponseProducts, která bude uchovávat produkty, které patří do určité kategorie. Struktura této třídy je jednoduchá: obsahuje vnořenou třídu DataProducts, která má vlastnost ProductsInCategory, což je pole produktů. Tato třída je navržena tak, aby byla schopná přijímat data ve formátu, který poskytuje GraphQL server.

csharp
using Packt.Shared; // Product
namespace Northwind.Mvc.GraphQLClient.Models { public class ResponseProducts { public class DataProducts { public Product[]? ProductsInCategory { get; set; } } public DataProducts? Data { get; set; } } }

Podobně byla definována i třída ResponseCategories, která uchovává informace o kategoriích produktů. Je opět strukturována tak, že vnořená třída DataCategories obsahuje pole kategorií.

csharp
using Packt.Shared; // Category namespace Northwind.Mvc.GraphQLClient.Models { public class ResponseCategories { public class DataCategories { public Category[]? Categories { get; set; } } public DataCategories? Data { get; set; } } }

Dále jsme přistoupili k vytvoření třídy IndexViewModel, která bude sloužit jako model pro naši stránku, kde se budou zobrazovat produkty a kategorie. Tento model obsahuje různé vlastnosti pro uchovávání informací, jako jsou HTTP status kód, surový odpovědní text, produkty, kategorie a chyby.

csharp
using Packt.Shared; // Product using System.Net; // HttpStatusCode namespace Northwind.Mvc.GraphQLClient.Models { public class IndexViewModel { public HttpStatusCode Code { get; set; }
public string? RawResponseBody { get; set; }
public Product[]? Products { get; set; } public Category[]? Categories { get; set; } public Error[]? Errors { get; set; } } }

Pro správnou komunikaci s GraphQL serverem jsme v souboru Program.cs přidali registraci HTTP klienta, který bude komunikovat s naším GraphQL serverem. Tento klient bude připraven pro odesílání POST požadavků s GraphQL dotazy, kde tělo požadavku bude obsahovat samotný GraphQL dotaz v JSON formátu.

csharp
builder.Services.AddHttpClient(name: "Northwind.GraphQL", configureClient: options =>
{ options.BaseAddress = new Uri("https://localhost:5111/"); options.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json", 1.0)); });

Ve třídě HomeController.cs definujeme pole pro uchování HTTP klienta, který bude použit pro odesílání požadavků na GraphQL server. V konstruktoru třídy HomeController je klient inicializován prostřednictvím dependency injection.

csharp
protected readonly IHttpClientFactory clientFactory;
public HomeController(ILogger logger, IHttpClientFactory clientFactory) { _logger = logger; this.clientFactory = clientFactory; }

V samotné metodě Index jsme přidali asynchronní volání pro komunikaci s GraphQL serverem. Nejprve provádíme GET požadavek na kořenovou adresu služby, abychom zjistili, zda server funguje správně. Poté odesíláme POST požadavek na GraphQL endpoint, kde v těle požadavku specifikujeme dotaz na produkty v určité kategorii.

csharp
public async Task Index(string id = "1") { IndexViewModel model = new(); try { HttpClient client = clientFactory.CreateClient(name: "Northwind.GraphQL"); HttpRequestMessage request = new(HttpMethod.Get, requestUri: "/"); HttpResponseMessage response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) { model.Code = response.StatusCode;
model.Errors = new[] { new Error { Message = "Service is not successfully responding to GET requests." } };
return View(model); } request = new(HttpMethod.Post, requestUri: "graphql"); request.Content = new StringContent( content: $$$""" { "query": "{productsInCategory(categoryId:{{{id}}}){productId productName unitsInStock}}" } """, encoding: Encoding.UTF8, mediaType: "application/json" ); response = await client.SendAsync(request); model.Code = response.StatusCode; model.RawResponseBody = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { model.Products = (await response.Content.ReadFromJsonAsync<ResponseProducts>())?.Data?.ProductsInCategory; } else { model.Errors = (await response.Content.ReadFromJsonAsync<ResponseErrors>())?.Errors; } } catch (Exception ex) { _logger.LogWarning($"Northwind.GraphQL service exception: {ex.Message}"); model.Errors = new[] { new Error { Message = ex.Message } }; } return View(model); }

Tato metoda nejen že zajišťuje komunikaci s GraphQL serverem, ale také správně zpracovává odpověď a zobrazuje chyby, pokud se nějaké vyskytly.

V samotné šabloně Index.cshtml jsme připravili zobrazení, které vykresluje produkty a kategorie na základě získaných dat. Pokud došlo k nějakým chybám, uživatel je o nich informován a mohou být zobrazeny podrobnosti o každé chybě, včetně cesty, kde k chybě došlo.

html
@model IndexViewModel @ViewData["Title"] = "Products from GraphQL service" <h2>@ViewData["Title"]</h2> @if (Model.Errors is not null) { @foreach (Error error in Model.Errors) { <div>@error.Message</div> @if (error.Path is not null) { @foreach (string path in error.Path) { <div>@path</div> } } } } @if (Model.Categories is not null) { <div>There are @Model.Categories.Count() categories</div> @foreach (Category category in Model.Categories) { <div>@category.CategoryId - @category.CategoryName</div> } } @if (Model.Products is not null) { <div>There are @Model.Products.Count() products</div> @foreach (Product p in Model.Products) {
<div>@p.ProductId - @p.ProductName - @(p.UnitsInStock is null ? "0" : p.UnitsInStock.Value) in stock</div>
} }

Při práci s GraphQL je kladeno velké důraz na správné formulování dotazů a manipulaci s odpověďmi. Důležité je nejen správně formulovat GraphQL dotaz, ale i správně zpracovávat odpovědi a efektivně vykreslovat data uživatelskému rozhraní. Výhody GraphQL spočívají ve flexibilitě dotazování a možnosti kombinovat data z různých zdrojů do jednoho odpovědního objektu.