ASP.NET s podporou knihovny React
29.07.2023Založení ASP.NET projektu s podporou React knihovny a TypeScriptu, včetně nastavení error handlingu pomocí ProblemDetails objektu na straně backendu.
Slovníček
React | JavaScriptová frontend knihovna pro tvorbu single-page aplikací (SPA). |
SPA | Frontend aplikace napsaná v JavaScriptu, kterou webový server odešle uživateli. Tato aplikace pak komunikuje se serverem výhradně prostřednictvím API a sama si generuje HTML a CSS. Jednou z výhod oproti přístupu, kdy server posílá celou stránku, je rychlejší odezva. |
ASP.NET | Open-source a multiplatformní webový framework pro .NET. |
TypeScript | Nadstavba nad JavaScriptem, která přidává typování a rozšiřuje jeho funkcionalitu. |
React Query | Knihovna, která zjednodušuje načítání a správu dat z API. |
Axios | JavaScriptový HTTP klient pro komunikaci s API. |
Node.js | JavaScriptový runtime. Node.js používá V8 runtime engine, který je integrován v prohlížečích, například v Chrome a Edge. Díky tomu JavaScript funguje v prohlížeči, i když uživatel nemá runtime nainstalovaný v operačním systému. |
CORS | CORS (Cross-Origin Resource Sharing) je mechanismus, který umožňuje provádět AJAX požadavky na jinou adresu, než je adresa načtené stránky v prohlížeči. Ve výchozím stavu prohlížeč takové požadavky nepovoluje. Webový server ale může prohlížeč instruovat, aby povolil cross-origin požadavky z určitých adres na jeho vlastní adresu. CORS kontrola slouží proti neoprávněným cross-origin požadavkům a přispívá k ochraně před CSRF/XSRF útoky. |
AJAX | Asynchronous JavaScript and XML (AJAX) je technologie, která umožňuje výměnu dat mezi prohlížečem a serverem na pozadí stránky, aniž by bylo nutné ji znovu načítat. JavaScript odešle HTTP požadavek a zpracuje odpověď. | Problem Details | Specifikace RFC 7807: Problem Details for HTTP APIs, jak by měl vypadat chybový objekt v HTTP API. |
Obsah
Úvod
Use case, který budu popisovat, počítá s tím, že frontend a backend poběží na stejném webovém serveru a chceme mít frontend i backend společně v jednom .NET projektu. Založíme projekt a připravíme backend, kde povolíme CORS, upravíme serializaci payloadu a následně nastavíme pravidla pro error handling.
Backend
K založení projektu budeme potřebovat Visual Studio 2022 a Node.js. Po instalaci Node.js zaktualizujeme balíčkovacího manažera.
npm install -g npm
Založení projektu
Vytvoříme solution:dotnet new solution --output Suppliers

Supplires\Web> ren ClientApp ClientAppOrig
Supplires\Web> npx create-react-app clientapp --template typescript
Supplires\Web> ren clientapp ClientApp

"scripts": {
"prestart": "node aspnetcore-https && node aspnetcore-react",
"start": "rimraf ./build && react-scripts start",
"build": "react-scripts build",
"test": "cross-env CI=true react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint ./src/"
}

Povolení CORS
Protože nám během vývoje běží backend i frontend na různých adresách, musíme na serveru v Program.cs povolit cross-origin požadavky.app.UseRouting();
if (app.Environment.IsDevelopment()) app.UseCors(c => c.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin());
Serializace payloadu
Pokud máme endpoint, který čte HTTP payload pomocí atributu FromBody:[HttpPost]
public IActionResult Post([FromBody] CreateCommand command)
builder.Services.AddControllersWithViews().AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
});
Problem Details
Od verze .NET 7 je k dispozici middleware, který neodchycené výjimky vrací jako JSON objekt podle RFC specifikace. V nižších verzích bylo nutné vytvořit vlastní middleware nebo nainstalovat externí NuGet balíček. Oba způsoby dobře popisuje Andrew Lock zde a zde.Do Program.cs přidáme dvě nové metody. První zaregistruje službu a zároveň upraví odpověď tak, aby obsahovala identifikátor TraceId. Ten lze zobrazit uživateli a použít k dohledání chybové hlášky v logu. Druhá metoda registruje související middleware.
private static void AddProblemDetails(WebApplicationBuilder builder)
{
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = (context) =>
{
// Add traceId property if not present.
if (!context.ProblemDetails.Extensions.ContainsKey("traceId"))
{
string? traceId = Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;
context.ProblemDetails.Extensions.Add(new KeyValuePair<string, object?>("traceId", traceId));
}
}
);
}
private static void UseProblemDetails(WebApplication app)
{
// By default it will use problem details service to generate a response.
// If problem details service is not registered, the code will not compile.
app.UseExceptionHandler();
// If not used, server does not return response data and browser will display default page.
// If used, server will return plain text, eg. "Status Code: 404; Not Found".
// If used and problem details service is registered, it will return problem details JSON object.
app.UseStatusCodePages();
}
// Register the service.
AddProblemDetails(builder);
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Add related middlewares.
UseProblemDetails(app);
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
"title": "An error occurred while processing your request.",
"status": 500,
"traceId": "00-f295935a77f29d4ad98159f7807cd785-da67c41344fb2161-00"
}
public IActionResult Get()
{
return Problem(detail: "Supplier does not exist.", statusCode: 404);
}
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"detail": "Supplier does not exist.",
"traceId": "00-aa127a7034a652229d622556315d1c18-609ce6911edc5f2c-00"
}
Validation Problem Details
Existuje také třída ValidationProblemDetails, která dokáže vygenerovat ProblemDetails z objektu ModelState. Pokud například použijeme atribut Required v input modelu:public class FooCommand
{
[Required]
public string Bar { get; set; } = null!;
}
[HttpPost]
public IActionResult Post([FromBody] FooCommand command)
{
// ...
}
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-77e6a72d78280bcb2cc05ffb018d9c32-18efd333f17af08b-00",
"errors": {
"Bar": [
"The Name field is required."
]
}
}
ModelState.AddModelError("error key", "error message");
if (!ModelState.IsValid) return ValidationProblem();
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-b0da05020e6d173a2a62ae0dd6c43365-a28deb9cc9f2057e-00",
"errors": {
"error key": [
"error message"
]
}
}
Launch profil
Ve výchozím stavu se při spuštění projektu spustí jak backend, tak frontend. To však není praktické pro vývoj, a proto si vytvoříme nový launch profil, který spustí pouze backend. Díky tomu budeme moci backend a frontend ovládat samostatně podle potřeby. Upravíme soubor Web > Properties > launchSettings.json, zkopírujeme stávající profil Web a vytvoříme nový profil BackendOnly. V něm odstraníme položku "launchBrowser": true nebo ji nastavíme na false."profiles": {
"Web": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:7085;http://localhost:5160",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
}
},
"BackendOnly": {
"commandName": "Project",
"launchBrowser": false,
"applicationUrl": "https://localhost:7085;http://localhost:5160",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
}
}
}


npm run start
Závěr
Ukázali jsme si, jak založit a nakonfigurovat ASP.NET projekt s React frameworkem a podporou TypeScriptu. V backendu jsem se zaměřil především na třídu Problem Details, která zjednodušuje a standardizuje zpracování chybových API odpovědí na frontendu. Nastavení Reactu, zpracování a volání API se pak pokusím popsat v dalším článku. 🚀