React s ASP.NET - První část
29.07.2023Založení projektu s podporou TypeScriptu a příprava backendu.
Slovníček
React | JavaScriptová frontend knihovna pro tvorbu single page aplikací (SPA). |
SPA | Ve zkratce se jedná o typ aplikace, kde server uživateli odešle celou aplikaci napsanou v JavaScriptu a ta pak komunikuje se serverem pouze pomocí API a HTML/CSS si generuje sama. Standardně server posílá celou stránku, kterou prohlížeč uživateli pouze zobrazí. Výhodou z pohledu uživatele může být lepší responsivita stránky. |
ASP.NET | Open-source a cross-platform webový framework pro .NET |
TypeScript | Nadstavba nad JavaScriptem, která přidává typování a další funkcionality. |
React Query | Knihovna, která zjednodušuje načítání a správu dat z API. |
Axios | JavaScript HTTP client. |
NodeJS | JavaScriptový runtime. NodeJS používá V8 runtime/engine, který mají prohlížeče (Chrome, Edge) již integrovaný. Proto JavaScript funguje v prohlížeči i pokud uživatel nemá runtime nainstalovaný. |
CORS | Cross-origin resource sharing. Cross-origin je AJAX požadavek 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, že má cross-origin požadavky z určitých adres na jeho adresu povolit. CORS kontrola slouží k zabránění CSRF/XSRF útoku. |
AJAX | Asynchronous JavaScript and XML. Jedná se o výměnu dat mezi prohlížečem a serverem na pozadí stránky bez nutnosti její načtení. JavaScript pošle HTTP požadavek a zpracuje odpověď. | Problem Details | RFC specifikace jak má vypadat chybový objekt z 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. Nejdříve si založíme projekt a připravíme backend, kde povolíme CORS, upravíme serializaci payloadu a nastavíme pravidla pro error handling. V navazujícím članku bych se pak chtěl věnovat připravě frontendu.
Backend
K založení projektu budeme potřebovat Visual Studio 2022 a NodeJS. Po instalaci NodeJS zaktualizujeme balíčkovacího manažera.
npm install -g npm
Založení projektu
Vytvoříme solutiondotnet 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 při vývoji backend i frontend beží na jiný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 potřeba si 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í metoda registruje službu a je upravená tak, aby do odpovědi přidala i TraceId identifikátor, který můžeme zobrazit uživateli a podle kterého pak můžeme dohledat detail chybové hlášky v logu. Druhá metoda pak 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 ještě třída ValidationProblemDetails, která umí vygenerovat ProblemDetails z ModelState objektu. Pokud například použijeme atribut Required v input modelupublic 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, pokud spustíme projekt, se nám spustí backend i frontend. To je nepraktické pro vývoj a proto si vytvoříme nový launch profil, který nám spustí pouze backend. Budeme tak moci backend a frontend ovládat zvlášť dle potřeby. Upravíme soubor Web > Properties > launchSettings.json a zkopírujeme stávající Web profil do nového BackendOnly profilu a umažeme "launchBrowser": true, případně 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 hlavně věnoval Problem Details třídě, která zjednodušuje a standardizuje zpracovávání API odpovědí na frontedu. Nastavení Reactu, zpracovávání a volání API se pak pokusím popsat v dalším článku. 🚀