Architektura Aplikacji Internetowych / Programowanie .NET

Rok akademicki: 2026/27

Zadanie laboratoryjne: Portal Zarządzania Flotą i Zgłoszeniami "TransitDesk" (ASP.NET Core MVC, Potok Middleware, Tag Helpers, Walidacja)

Prowadzący: Tomasz Goluch
Wersja: 1.1
Wymagany standard: C# 12 / .NET 8+
Maksymalna ocena zadania: 100 punktów (100%)
Forma realizacji: Projekt tworzony całkowicie od zera (Greenfield)


1. Kontekst Biznesowy i Architektoniczny Projektu

Projektujesz system centralny miejskiego przedsiębiorstwa transportowego – TransitDesk.Web.

Aplikacja stanowi wieloplatformowy portal dyspozytorski oraz punkt kontaktu dla pasażerów, realizujący ewidencję taboru autobusowo-tramwajowego, przyjmowanie zgłoszeń awarii i incydentów drogowych oraz publikację komunikatów taryfowych.

W aplikacjach webowych o wysokim natężeniu ruchu klasyczne podejścia architektoniczne rodzą poważne wyzwania:

Twoim celem jest zaprojektowanie modularnej, bezpiecznej i przetestowanej aplikacji internetowej w technologii ASP.NET Core MVC (.NET 8/9). Projekt opiera się na silnie typowanych modelach widoku (ViewModels), semantycznych Tag Helpers, deklaratywnym routingu z własnymi ograniczeniami (Route Constraints), asynchronicznym potoku I/O oraz pełnej izolacji stanu sesji.


2. Inicjalizacja Środowiska (Krok 0: Przygotowanie Solucji od Zera)

Otwórz terminal w pustym katalogu roboczym i skonfiguruj strukturę solucji z podziałem na aplikację webową, bibliotekę domenową oraz pakiet testów automatycznych:

# 1. Utworzenie pustego pliku rozwiązania
dotnet new sln -n TransitDesk

# 2. Utworzenie projektu rdzenia domeny i modeli (Class Library)
dotnet new classlib -n TransitDesk.Core -f net8.0

# 3. Utworzenie głównego projektu aplikacji internetowej (ASP.NET Core MVC)
dotnet new mvc -n TransitDesk.Web -f net8.0 --auth None

# 4. Utworzenie projektu testów jednostkowych i integracyjnych (xUnit)
dotnet new xunit -n TransitDesk.Tests -f net8.0

# 5. Dołączenie projektów do pliku rozwiązania
dotnet sln add TransitDesk.Core/TransitDesk.Core.csproj
dotnet sln add TransitDesk.Web/TransitDesk.Web.csproj
dotnet sln add TransitDesk.Tests/TransitDesk.Tests.csproj

# 6. Konfiguracja powiązań między projektami
dotnet add TransitDesk.Web/TransitDesk.Web.csproj reference TransitDesk.Core/TransitDesk.Core.csproj
dotnet add TransitDesk.Tests/TransitDesk.Tests.csproj reference TransitDesk.Core/TransitDesk.Core.csproj
dotnet add TransitDesk.Tests/TransitDesk.Tests.csproj reference TransitDesk.Web/TransitDesk.Web.csproj

# 7. Instalacja bibliotek testowych i asercji płynnych
dotnet add TransitDesk.Tests/TransitDesk.Tests.csproj package Microsoft.AspNetCore.Mvc.Testing
dotnet add TransitDesk.Tests/TransitDesk.Tests.csproj package FluentAssertions

💡 Zalecenie architektoniczne: Rola projektu TransitDesk.Core a brak bazy danych
Na tym laboratorium nie korzystamy jeszcze z Entity Framework Core ani fizycznej bazy danych. Zalecaną dobrą praktyką architektoniczną jest umieszczenie czystych encji domenowych (np. Vehicle, IncidentReport) oraz kontraktów serwisów w projekcie TransitDesk.Core, podczas gdy w TransitDesk.Web definiowane są wyłącznie modele formularzy (ViewModels).
Uwaga: Na tym etapie nie jest to twardo egzekwowany wymóg. Dopuszczalnym uproszczeniem jest przechowywanie danych testowych w pamięci podręcznej serwisu lub kontrolera (kolekcje in-memory), a projekt TransitDesk.Core można potraktować jako współdzieloną bibliotekę klas.


3. Szczegółowe Wymagania i Podział Punktów (Łącznie: 100 pkt)

┌───────────────────────────────────────────────────────────────────────────────────┐
│                   ARCHITEKTURA SYSTEMU TRANSITDESK (ASP.NET CORE)                 │
├───────────────────────────────────────────────────────────────────────────────────┤
│  [Część 1: 15 pkt] Potok Middleware      ──► Minimal Hosting / Re-Execute Errors  │
│  [Część 2: 15 pkt] Routing & Constraints ──► Konwencjonalny + Atrybutowy + IRoute │
│  [Część 3: 15 pkt] Formularze & Razor    ──► ViewModel + Tag Helpers + Wzorzec PRG│
│  [Część 4: 15 pkt] Walidacja Hybrydowa   ──► DataAnnotations + Remote AJAX Valid. │
│  [Część 5: 15 pkt] Bezpieczny Upload     ──► IFormFile Async + Path Traversal G.  │
│  [Część 6: 10 pkt] Zarządzanie Stanem    ──► ISession JSON + Polityka Ciasteczek  │
│  [Część 7: 15 pkt] Filtry & Kontenery    ──► Action Filters + IViewLocalizer + DCK│
└───────────────────────────────────────────────────────────────────────────────────┘

Część 1: Potok Middleware, Minimal Hosting i Re-ekzekucja Błędów HTTP (15 pkt)

Prawidłowo skonfigurowany potok przetwarzania żądań w Program.cs stanowi pierwszą linię obrony aplikacji. Należy zaimplementować deterministyczny łańcuch middleware oraz mechanizm eleganckiej obsługi kodów statusu bez utraty oryginalnego nagłówka HTTP.

Wymagania szczegółowe:
  1. [4 pkt] Konfiguracja i rejestracja usług w Program.cs:
    • Rejestracja MVC (AddControllersWithViews).
    • Rejestracja pamięci rozproszonej (AddDistributedMemoryCache) i sesji z czasem wygaśnięcia 15 minut oraz flagami Cookie.HttpOnly = true i Cookie.IsEssential = true.
  2. [6 pkt] Architektura i kolejność potoku HTTP:
    • Ścisłe uporządkowanie middleware: środowiskowa obsługa wyjątków (UseDeveloperExceptionPage w trybie deweloperskim, UseExceptionHandler + UseHsts w trybie produkcyjnym) → UseHttpsRedirection → UseStaticFiles → UseRouting → UseSession → UseAuthentication → UseAuthorization → MapControllerRoute.
  3. [5 pkt] Re-ekzekucja kodów błędów HTTP:
    • Zarejestruj UseStatusCodePagesWithReExecute("/Errors/Status", "?code={0}").
    • Zaimplementuj ErrorsController z akcją Status(int code) zwracającą dedykowany widok dla błędów 404 (Brak zasobu) i 403 (Odmowa dostępu).
    • Odpowiedź wysyłana do przeglądarki musi zachować oryginalny status błędu (np. nagłówek 404 Not Found, a nie 200 OK czy przekierowanie 302).
💡 Wskazówki implementacyjne: Re-ekzekucja kodów statusu (Rozwiń)
// W pliku Program.cs (przed UseRouting i UseStaticFiles):
app.UseStatusCodePagesWithReExecute("/Errors/Status", "?code={0}");

// W Controllers/ErrorsController.cs:
public class ErrorsController : Controller
{
    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Status(int code)
    {
        ViewBag.StatusCode = code;
        ViewBag.Message = code switch
        {
            404 => "Nie odnaleziono wskazanego zasobu taboru lub zgłoszenia.",
            403 => "Brak uprawnień dostępu do modułu dyspozytorskiego.",
            _ => "Wystąpił nieoczekiwany błąd protokołu HTTP."
        };
        return View();
    }
}

Część 2: Routing Hybrydowy i Własne Ograniczenie Trasy (IRouteConstraint) (15 pkt)

Aplikacja musi obsługiwać zarówno intuicyjne trasy publiczne, jak i ściśle ograniczone adresy techniczne dla personelu serwisowego.

Struktura katalogów i przestrzeń nazw dla ograniczenia trasy:
Klasa ograniczenia trasy implementuje interfejs IRouteConstraint z biblioteki Microsoft.AspNetCore.Routing. Jest detalem infrastruktury webowej, dlatego powinna znaleźć się w projekcie TransitDesk.Web:
Ścieżka: TransitDesk.Web/Routing/Constraints/FleetSideNumberConstraint.cs
Przestrzeń nazw: namespace TransitDesk.Web.Routing.Constraints;
(Dzięki temu projekt domenowy TransitDesk.Core pozostaje czysty i niezależny od bibliotek ASP.NET Core).

Wymagania szczegółowe:
  1. [4 pkt] Dedykowana trasa konwencjonalna:
    • Zdefiniuj w Program.cs trasę o nazwie depot_fleet: Wzorzec: "Zajezdnia/{depotName}/{tractionType?}" mapujący na kontroler VehiclesController i akcję DepotFleetList.
  2. [7 pkt] Niestandardowy Constraint numeru bocznego (FleetSideNumberConstraint):
    • Utwórz klasę FleetSideNumberConstraint implementującą interfejs IRouteConstraint.
    • Logika walidacji: numer boczny taboru musi być wartością liczbową z zakresu 1000–9999 (standard gdańskich pojazdów GAiT).
    • Zarejestruj ograniczenie w kontenerze pod kluczem fleetSideNo.
  3. [4 pkt] Routing atrybutowy z ograniczeniem złożonym:
    • W VehiclesController utwórz akcję Details oznaczoną trasą atrybutową: [HttpGet("pojazd/{sideNumber:fleetSideNo}")].
    • Wszelkie zapytania niespełniające kryteriów (np. /pojazd/999, /pojazd/abc) muszą zostać odrzucone na etapie routingu (skutkując kodem HTTP 404 przechwyconym przez moduł z Części 1).
💡 Wskazówki implementacyjne: Rejestracja IRouteConstraint (Rozwiń)
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Http;
using System.Globalization;

namespace TransitDesk.Web.Routing.Constraints;

public class FleetSideNumberConstraint : IRouteConstraint
{
    public bool Match(
        HttpContext? httpContext,
        IRouter? route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        if (values.TryGetValue(routeKey, out var rawVal) && rawVal != null)
        {
            if (int.TryParse(rawVal.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int number))
            {
                return number >= 1000 && number <= 9999;
            }
        }
        return false;
    }
}

// Rejestracja w Program.cs:
builder.Services.Configure<RouteOptions>(options =>
{
    options.ConstraintMap.Add("fleetSideNo", typeof(FleetSideNumberConstraint));
});

Część 3: Silnie Typowane Widoki, ViewModel i Semantyczne Tag Helpers (15 pkt)

Zbuduj moduł wprowadzania nowego pojazdu do ewidencji floty w oparciu o czystą architekturę widoków i nowoczesne Tag Helpers (całkowity zakaz stosowania starych metod @Html.TextBoxFor i @Html.LabelFor).

Architektura modeli (Zalecenie vs Wariant minimum):

Wymagania szczegółowe:
  1. [4 pkt] Projekt modelu widoku (VehicleViewModel):
    • Model musi zawierać właściwości: SideNumber (int), ModelName (string), DepotName (string), ProductionYear (int), Capacity (int), IsLowFloor (bool – niskopodłogowy) oraz ServiceStatus (enum).
  2. [7 pkt] Semantyczny formularz Razor z Tag Helpers:
    • Plik widoku Views/Vehicles/Create.cshtml wykorzystujący Tag Helpers: <form asp-action="Create" method="post">, <input asp-for="...">, <label asp-for="...">, <select asp-for="..."> oraz <span asp-validation-for="...">.
    • Zastosowanie tag helpera <div asp-validation-summary="ModelOnly" class="text-danger"></div> do prezentacji błędów globalnych.
  3. [4 pkt] Wzorzec Post-Redirect-Get (PRG):
    • W przypadku błędów walidacji akcja POST natychmiast zwraca ten sam widok z przekazanym modelem.
    • Po poprawnym zapisie pojazdu akcja zapisuje powiadomienie w TempData["SuccessMessage"] i wykonuje RedirectToAction(nameof(Index)).
    • Widok Index.cshtml odczytuje TempData i renderuje powiadomienie (np. Bootstrap alert).
💡 Wskazówki implementacyjne: Widok formularza z Tag Helpers (Rozwiń)
@model TransitDesk.Web.ViewModels.VehicleViewModel

@{
    ViewData["Title"] = "Rejestracja Nowego Pojazdu";
}

<h2>@ViewData["Title"]</h2>

<form asp-controller="Vehicles" asp-action="Create" method="post">
    <div asp-validation-summary="ModelOnly" class="alert alert-danger"></div>

    <div class="mb-3">
        <label asp-for="SideNumber" class="form-label"></label>
        <input asp-for="SideNumber" class="form-control" />
        <span asp-validation-for="SideNumber" class="text-danger"></span>
    </div>

    <div class="mb-3">
        <label asp-for="ModelName" class="form-label"></label>
        <input asp-for="ModelName" class="form-control" />
        <span asp-validation-for="ModelName" class="text-danger"></span>
    </div>

    <div class="mb-3 form-check">
        <input asp-for="IsLowFloor" class="form-check-input" />
        <label asp-for="IsLowFloor" class="form-check-label"></label>
    </div>

    <button type="submit" class="btn btn-primary">Zapisz pojazd w rejestrze</button>
</form>

Część 4: Zaawansowany Model Binding, Walidacja Hybrydowa i Zdalna (Remote) (15 pkt)

Zaimplementuj moduł zgłaszania incydentów drogowych (IncidentReportViewModel), który chroni bazę danych przed wprowadzeniem niespójnych informacji za pomocą walidacji hybrydowej (klient + serwer) oraz zdalnej (Remote via AJAX).

Wymagania szczegółowe:
  1. [5 pkt] Deklaratywne adnotacje danych (DataAnnotations):
    • LineNumber: pole wymagane, format alfanumeryczny od 1 do 4 znaków (np. 6, 199, N1) z użyciem [RegularExpression].
    • Description: minimalna długość 20 znaków, maksymalna 500 znaków ([StringLength]).
    • OccurredAt: data i czas zdarzenia ([DataType(DataType.DateTime)]), nie może być datą z przyszłości.
  2. [6 pkt] Walidacja zdalna numeru taborowego ([Remote]):
    • Pole VehicleSideNumber w modelu widoku musi posiadać atrybut: [Remote(action: "VerifyVehicleAvailable", controller: "Incidents", ErrorMessage = "Pojazd o podanym numerze nie istnieje lub jest wycofany.")]
    • W IncidentsController zaimplementuj akcję VerifyVehicleAvailable(int vehicleSideNumber) zwracającą Json(true) w przypadku sukcesu lub Json(błąd) w przypadku braku pojazdu w ewidencji.
  3. [4 pkt] Walidacja po stronie klienta (Unobtrusive Validation):
    • W widoku zgłoszenia dołącz skrypty walidacyjne za pomocą sekcji widoku: @section Scripts { <partial name="_ValidationScriptsPartial" /> }
    • Formularz musi uniemożliwiać wysyłkę z błędnymi danymi bez przeładowania strony w przeglądarce.
💡 Wskazówki implementacyjne: Walidacja zdalna w kontrolerze (Rozwiń)
[HttpGet]
public IActionResult VerifyVehicleAvailable(int vehicleSideNumber)
{
    // Logika weryfikacji w kolekcji floty
    bool existsAndActive = _vehicleService.IsActiveVehicle(vehicleSideNumber);
    if (!existsAndActive)
    {
        return Json($"Pojazd #{vehicleSideNumber} nie figuruje w czynnym rejestrze floty.");
    }
    return Json(true);
}

Część 5: Bezpieczny Asynchroniczny Upload Plików i Ochrona Zasobów (15 pkt)

Rozszerz formularz zgłaszania incydentów o możliwość załączenia dokumentacji fotograficznej lub raportu PDF. Zaimplementuj rygorystyczne procedury bezpieczeństwa I/O.

Wymagania szczegółowe:
  1. [4 pkt] Konfiguracja formularza:
    • Formularz HTML musi posiadać atrybut enctype="multipart/form-data".
    • Akcja kontrolera przyjmuje plik za pośrednictwem interfejsu IFormFile? attachmentFile.
  2. [7 pkt] Ochrona przed Path Traversal i weryfikacja zawartości:
    • Ograniczenie rozmiaru pliku: maksymalnie 5 MB (weryfikacja na serwerze).
    • Biała lista rozszerzeń: wyłącznie .jpg, .jpeg, .png, .pdf.
    • Zakaz używania oryginalnej nazwy pliku klienta na dysku: Zapisywany plik musi otrzymać kryptograficznie bezpieczną, unikalną nazwę opartą o Guid.NewGuid() z zachowaniem zweryfikowanego rozszerzenia.
  3. [4 pkt] Asynchroniczne strumieniowanie I/O:
    • Zapis do folderu App_Data/Uploads/ (znajdującego się poza publicznym folderem wwwroot).
    • Zastosowanie asynchronicznego strumienia await attachmentFile.CopyToAsync(fileStream) bez blokowania wątków puli Kestrel.
💡 Wskazówki implementacyjne: Bezpieczny upload plików (Rozwiń)
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ReportIncident(IncidentReportViewModel model, IFormFile? attachmentFile)
{
    if (!ModelState.IsValid) return View(model);

    if (attachmentFile != null && attachmentFile.Length > 0)
    {
        if (attachmentFile.Length > 5 * 1024 * 1024)
        {
            ModelState.AddModelError("attachmentFile", "Rozmiar załącznika nie może przekraczać 5 MB.");
            return View(model);
        }

        var allowedExt = new[] { ".jpg", ".jpeg", ".png", ".pdf" };
        var ext = Path.GetExtension(attachmentFile.FileName).ToLowerInvariant();
        if (!allowedExt.Contains(ext))
        {
            ModelState.AddModelError("attachmentFile", "Niedozwolony typ pliku.");
            return View(model);
        }

        var secureFileName = $"{Guid.NewGuid()}{ext}";
        var storageDir = Path.Combine(Directory.GetCurrentDirectory(), "App_Data", "Uploads");
        Directory.CreateDirectory(storageDir);

        var targetPath = Path.Combine(storageDir, secureFileName);
        using (var stream = new FileStream(targetPath, FileMode.Create))
        {
            await attachmentFile.CopyToAsync(stream);
        }
        model.AttachmentPath = targetPath;
    }

    await _incidentService.RegisterIncidentAsync(model);
    return RedirectToAction("Success");
}

Część 6: Zarządzanie Stanem: Sesja JSON, Polityka Ciasteczek i Wzorzec PRG (10 pkt)

Aplikacja musi umożliwiać dyspozytorowi tworzenie tymczasowej „listy monitorowanych pojazdów” (koszyk pojazdów) w stanie sesji bez konieczności trwałego zapisu w bazie danych.

Wymagania szczegółowe:
  1. [4 pkt] Metody rozszerzające sesji oparte o System.Text.Json:
    • Przygotuj klasę SessionExtensions dostarczającą metody SetObject<T>(this ISession session, string key, T value) oraz GetObject<T>(this ISession session, string key).
  2. [4 pkt] Obsługa listy monitorowanych pojazdów:
    • Akcja AddVehicleToWatchlist(int sideNumber) pobiera obiekt z sesji, dopisuje pojazd i zapisuje stan z powrotem do sesji.
    • Pasek nawigacyjny w pliku _Layout.cshtml odczytuje z sesji liczbę aktualnie monitorowanych pojazdów i wyświetla dynamiczny licznik obok pozycji menu.
  3. [2 pkt] Zgodność z polityką ciasteczek i RODO:
    • Poprawna konfiguracja CookiePolicyOptions w Program.cs gwarantująca, że ciasteczko sesyjne jest traktowane jako IsEssential = true oraz posiada flagę SameSiteMode.Lax.
💡 Wskazówki implementacyjne: Serializacja obiektów w sesji (Rozwiń)
using System.Text.Json;
using Microsoft.AspNetCore.Http;

public static class SessionExtensions
{
    public static void SetObject<T>(this ISession session, string key, T value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T? GetObject<T>(this ISession session, string key)
    {
        var rawJson = session.GetString(key);
        return rawJson == null ? default : JsonSerializer.Deserialize<T>(rawJson);
    }
}

Część 7: Filtry Akcji, Wielojęzyczność i Konteneryzacja w Dockerze (15 pkt)

Wdrożenie zadań przekrojowych (cross-cutting concerns), przygotowanie aplikacji do obsługi wielu języków oraz konteneryzacja środowiska wykonawczego.

Wymagania szczegółowe:
  1. [5 pkt] Niestandardowy filtr akcji (AuditLogAttribute):
    • Utwórz klasę dziedziczącą po ActionFilterAttribute.
    • W metodzie OnActionExecuting rejestruj: adres IP klienta (context.HttpContext.Connection.RemoteIpAddress), nazwę kontrolera, akcję oraz znacznik czasu UTC.
    • W metodzie OnActionExecuted obliczaj całkowity czas realizacji żądania i dopisuj go do nagłówka odpowiedzi HTTP X-Execution-Duration-Ms.
    • Oznacz filtrem krytyczne akcje edycji taboru i zgłaszania awarii.
  2. [5 pkt] Wielojęzyczność widoków (IViewLocalizer):
    • Skonfiguruj obsługę kultur pl-PL oraz en-US w Program.cs (RequestLocalizationOptions).
    • Utwórz pliki zasobów .resx dla strony głównej oraz menu nawigacyjnego.
    • Wstrzyknij @inject IViewLocalizer Localizer do pliku Views/Home/Index.cshtml i przetłumacz nagłówki oraz opisy powitalne.
  3. [5 pkt] Skrypt konteneryzacji Docker:
    • Przygotuj produkcyjny plik Dockerfile oparty o wieloetapowy proces budowania (multi-stage build) dla środowiska .NET 8 ASP.NET Runtime.
    • Aplikacja w kontenerze musi nasłuchiwać na porcie HTTP 8080.
💡 Wskazówki implementacyjne: Filtr audytu i metryk czasu (Rozwiń)
using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;

public class AuditLogAttribute : ActionFilterAttribute
{
    private Stopwatch? _stopwatch;

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        _stopwatch = Stopwatch.StartNew();
        var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString();
        var action = context.ActionDescriptor.DisplayName;
        Debug.WriteLine($"[AUDIT START] IP: {ip} | Akcja: {action} | UTC: {DateTime.UtcNow}");
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        _stopwatch?.Stop();
        context.HttpContext.Response.Headers.TryAdd("X-Execution-Duration-Ms", _stopwatch?.ElapsedMilliseconds.ToString());
    }
}

4. Wymagania Dotyczące Pakietu Testów Automatycznych

W projekcie TransitDesk.Tests przygotuj zestaw testów automatycznych weryfikujących integralność systemu:

  1. FleetSideNumberConstraintTests (Testy jednostkowe):
    • Weryfikacja poprawności ograniczenia trasy dla prawidłowych numerów (1000, 2072, 9999) → wynik true.
    • Weryfikacja odrzucania wartości nieprawidłowych (999, 10000, "abc", null) → wynik false.
  2. IncidentValidationTests (Testy jednostkowe modeli):
    • Weryfikacja poprawności walidacji IncidentReportViewModel za pomocą Validator.TryValidateObject.
    • Sprawdzenie, czy opis krótszy niż 20 znaków lub niepoprawny numer linii zgłasza błąd walidacji.
  3. MiddlewareAndStatusPagesTests (Testy integracyjne z WebApplicationFactory):
    • Wywołanie nieistniejącego adresu (np. /nieistnieje/strona) musi zwrócić status HTTP 404 wraz z treścią wyrenderowaną przez ErrorsController.
  4. SessionExtensionsTests:
    • Test sprawdzający, czy złożony obiekt listy pojazdów jest poprawnie serializowany i deserializowany bez utraty danych.

Polecenie uruchomienia testów weryfikacyjnych:

dotnet test --logger "console;verbosity=detailed"

5. Tabela Podsumowująca Oceny (Suma: 100 pkt / 100%)

Nr Zakres modułu Weryfikowane mechanizmy, technologie i standardy Punkty
1 Potok Middleware i Błędy HTTP Kolejność komponentów w Program.cs, UseStatusCodePagesWithReExecute, widok błędu z zachowaniem statusu 404/403. 15 pkt
2 Routing i Ograniczenia Tras Trasa konwencjonalna depot_fleet, implementacja IRouteConstraint (zakres 1000–9999), routing atrybutowy. 15 pkt
3 Widoki, ViewModel i Tag Helpers Silnie typowane modele widoku, semantyczne Tag Helpers w formularzu, wzorzec Post-Redirect-Get z TempData. 15 pkt
4 Model Binding i Walidacja Hybrydowa Atrybuty DataAnnotations, zdalna walidacja AJAX [Remote], skrypty walidacji po stronie klienta. 15 pkt
5 Bezpieczny Upload Plików IFormFile, asynchroniczny zapis CopyToAsync, biała lista rozszerzeń, ochrona przed Path Traversal (GUID). 15 pkt
6 Zarządzanie Stanem i Sesja JSON Metody rozszerzające ISession z biblioteką System.Text.Json, koszyk monitorowanych pojazdów w _Layout. 10 pkt
7 Filtry Akcji, Lokalizacja i Docker Filtr AuditLogAttribute z nagłówkiem HTTP, pliki .resx z IViewLocalizer, wieloetapowy Dockerfile. 15 pkt
SUMA 100 pkt (100%)