Dataczwartek, 13 sierpnia 2026 Czas18:16:59
← Aplikacje webowe

Lekcja 12. ASP.NET Core jako backend dla Angular — Web API i CORS

Trudny / egzaminacyjny

Po co się tego uczymy?

Masz już gotowy frontend w Angularze (lekcje 9-11) i wiesz, jak budować backend w ASP.NET Core (lekcje 3, wcześniejsze doświadczenia). Czas je POŁĄCZYĆ. Backend, który komunikuje się z frontendem SPA, nie zwraca gotowego HTML — zwraca WYŁĄCZNIE dane (najczęściej JSON), a Angular sam decyduje, jak je wyświetlić. Ta lekcja pokazuje Web API w ASP.NET Core oraz mechanizm CORS, bez którego Angular (inny port!) w ogóle nie mógłby połączyć się z Twoim backendem.

Teoria

Kontroler API dziedziczy po ControllerBase (nie po Controller, który dodatkowo obsługuje widoki HTML — Web API ich nie potrzebuje, bo cały interfejs buduje Angular). Atrybut [ApiController] nad klasą włącza automatyczną walidację modelu (jeśli dane są niepoprawne, ASP.NET Core SAM zwraca błąd 400, zanim kod w ogóle wejdzie do metody) i inne usprawnienia typowe dla API.

Atrybuty routingu HTTP — każda akcja jawnie deklaruje metodę HTTP, którą obsługuje:

Atrybut Metoda HTTP Typowe zastosowanie
[HttpGet] GET pobranie danych (lista albo pojedynczy element) — wywoływane przez Angular HttpClient.get()
[HttpPost] POST utworzenie nowego zasobu — HttpClient.post()
[HttpPut("{id}")] PUT pełna aktualizacja — HttpClient.put()
[HttpDelete("{id}")] DELETE usunięcie — HttpClient.delete()

CORS (Cross-Origin Resource Sharing) — dlaczego jest KONIECZNY w tym układzie. Twój Angular działa (podczas developmentu) pod adresem http://localhost:4200, a backend ASP.NET Core pod zupełnie INNYM adresem/portem (np. https://localhost:7042). Przeglądarka ze WZGLĘDÓW BEZPIECZEŃSTWA domyślnie BLOKUJE żądania JavaScript/TypeScript do INNEJ domeny/portu niż ta, z której strona została załadowana — to mechanizm CORS. Backend musi JAWNIE zezwolić na żądania z adresu Angulara: builder.Services.AddCors(opt => opt.AddPolicy("AngularApp", p => p.WithOrigins("http://localhost:4200").AllowAnyMethod().AllowAnyHeader())); oraz app.UseCors("AngularApp");. Bez tej konfiguracji Angular dostanie błąd w konsoli przeglądarki, mimo że backend "działa poprawnie" (bo problem jest po stronie reguł bezpieczeństwa przeglądarki, nie samego API).

Typy zwracane — ActionResult<T>. Ok(dane) (200, z danymi), NotFound() (404), BadRequest("komunikat") (400), CreatedAtAction(...) (201, po udanym POST) — te same kody odpowiedzi HTTP, które sprawdzasz po stronie Angulara w bloku .subscribe() albo try/catch przy async/await.

Konsumpcja API z Angulara — HttpClient. Angular ma wbudowaną usługę HttpClient (wstrzykiwaną przez konstruktor, dokładnie jak serwisy z lekcji 10) do wysyłania żądań HTTP: this.http.get<Produkt[]>('https://localhost:7042/api/produkty') zwraca Observable (strumień danych w czasie — koncepcja z biblioteki RxJS, używanej wewnętrznie przez Angular) — subskrybujesz go metodą .subscribe(dane => { ... }), żeby odebrać dane, gdy faktycznie dotrą z serwera.

Testowanie API narzędziem Postman (z lekcji 2) — zanim frontend Angular zacznie korzystać z Twojego API, warto samodzielnie sprawdzić w Postmanie, czy każdy endpoint zwraca oczekiwane dane.

Schemat

Żądanie GET /api/produkty
        │
        ▼
ProduktyApiController.GetWszystkie()  [HttpGet]
        │
        ▼
context.Produkty.ToListAsync()
        │
        ▼
return Ok(produkty);   → odpowiedź JSON: [{"id":1,"nazwa":"...","cena":89.99}, ...]

Żądanie POST /api/produkty (treść: JSON nowego produktu)
        │
        ▼
ProduktyApiController.Dodaj(Produkt nowy)  [HttpPost]
        │
        │  [ApiController] AUTOMATYCZNIE waliduje - jeśli błąd, zwraca 400 bez kodu w akcji
        ▼
context.Produkty.Add(nowy); SaveChangesAsync();
        │
        ▼
return CreatedAtAction(nameof(Pobierz), new { id = nowy.Id }, nowy);  → 201 Created

Klient (JavaScript fetch / aplikacja MAUI HttpClient / Postman)
        │
        ▼
odbiera odpowiedź JSON, przetwarza dalej

Przykład z życia

Aplikacja sklepowa w Angularze wyświetla listę produktów pobraną z Twojego własnego backendu ASP.NET Core — dokładnie ten sam mechanizm REST API, którego aplikacja MAUI używała jako klient w dziale mobilnym (HttpClient po stronie C#), tylko teraz klientem jest Angular (HttpClient po stronie TypeScript), a Ty budujesz OBIE strony tej komunikacji.

Controllers/ProduktyController.cs + Program.cs (CORS)

// Controllers/ProduktyController.cs
using Microsoft.AspNetCore.Mvc;

namespace MojBackend.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProduktyController : ControllerBase
{
    private static List<Produkt> _produkty = new()
    {
        new Produkt { Id = 1, Nazwa = "Klawiatura", Cena = 89.99 },
        new Produkt { Id = 2, Nazwa = "Monitor", Cena = 799.00 }
    };

    [HttpGet]
    public ActionResult<List<Produkt>> GetWszystkie() => Ok(_produkty);

    [HttpGet("{id}")]
    public ActionResult<Produkt> Pobierz(int id)
    {
        var produkt = _produkty.FirstOrDefault(p => p.Id == id);
        return produkt is null ? NotFound() : Ok(produkt);
    }

    [HttpPost]
    public ActionResult<Produkt> Dodaj(Produkt nowy)
    {
        nowy.Id = _produkty.Count >  ? _produkty.Max(p => p.Id) + 1 : 1;
        _produkty.Add(nowy);
        return CreatedAtAction(nameof(Pobierz), new { id = nowy.Id }, nowy);
    }
}

public class Produkt
{
    public int Id { get; set; }
    public string Nazwa { get; set; } = "";
    public double Cena { get; set; }
}

// ---------------------------------------------------------------------

// Program.cs - fragment: rejestracja CORS
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddCors(options =>
{
    options.AddPolicy("AngularApp", policy =>
        policy.WithOrigins("http://localhost:4200")
              .AllowAnyMethod()
              .AllowAnyHeader());
});

var app = builder.Build();

app.UseCors("AngularApp");   // WAŻNE: przed app.MapControllers()
app.MapControllers();

app.Run();

produkt.service.ts + produkty.component.ts

// produkt.service.ts - serwis Angular komunikujący się z backendem
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Produkt {
  id: number;
  nazwa: string;
  cena: number;
}

@Injectable({ providedIn: 'root' })
export class ProduktService {
  private adresApi = 'https://localhost:7042/api/produkty';

  constructor(private http: HttpClient) {}

  pobierzWszystkie(): Observable<Produkt[]> {
    return this.http.get<Produkt[]>(this.adresApi);
  }

  dodaj(produkt: Produkt): Observable<Produkt> {
    return this.http.post<Produkt>(this.adresApi, produkt);
  }
}

// ---------------------------------------------------------------------

// produkty.component.ts - użycie serwisu w komponencie
import { Component, OnInit } from '@angular/core';
import { ProduktService, Produkt } from '../produkt.service';

@Component({
  selector: 'app-produkty',
  standalone: true,
  templateUrl: './produkty.component.html'
})
export class ProduktyComponent implements OnInit {
  produkty: Produkt[] = [];

  constructor(private produktService: ProduktService) {}

  ngOnInit(): void {
    this.produktService.pobierzWszystkie().subscribe({
      next: (dane) => this.produkty = dane,
      error: (blad) => console.error('Błąd pobierania produktów:', blad)
    });
  }
}

Komentarz i wyjaśnienie kodu

app.UseCors("AngularApp") MUSI wystąpić PRZED app.MapControllers() w Program.cs — kolejność middleware w ASP.NET Core ma znaczenie; gdyby CORS był skonfigurowany PO routingu, żądania z Angulara nadal byłyby blokowane.

W ProduktService, metoda pobierzWszystkie() NIE zwraca gotowej listy produktów — zwraca Observable<Produkt[]>, czyli "obietnicę danych w przyszłości". Dopiero .subscribe({ next: ... }) w komponencie faktycznie URUCHAMIA żądanie i odbiera wynik, gdy backend odpowie. To trochę jak async/await z C#, tylko zrealizowane przez bibliotekę RxJS — Angular pozwala też pisać to przez async/await, ale .subscribe() jest bardziej "natywnym" stylem Angulara.

ngOnInit() (implementujące interfejs OnInit) to metoda cyklu życia komponentu Angular, wywoływana RAZ, zaraz po utworzeniu komponentu — dokładnie ten sam moment co OnInitializedAsync() w Blazor czy konstruktor PageModel w Razor Pages; to właściwe miejsce na pobranie początkowych danych z backendu.

Ćwiczenie samodzielne

Uzupełnij projekt z lekcji 3 o kontroler ProduktyController i konfigurację CORS jak w przykładzie. W projekcie Angular dodaj ProduktService i wykorzystaj go w komponencie listy produktów z lekcji 9 — sprawdź w przeglądarce (Narzędzia deweloperskie → Sieć), że dane faktycznie przychodzą z backendu, a NIE są już wpisane na sztywno w kodzie TypeScript.

Zadania do pracy własnej

  1. Dodaj do ProduktyController akcję [HttpDelete("{id}")] usuwającą produkt, i odpowiadającą jej metodę usun(id: number) w ProduktService.

  2. Dodaj obsługę błędów w komponencie Angular — jeśli backend jest niedostępny (np. nieuruchomiony), pokaż w interfejsie komunikat "Nie udało się połączyć z serwerem" zamiast pustej, milczącej listy.

  3. Skonfiguruj CORS TYLKO dla konkretnych metod i nagłówków (zamiast AllowAnyMethod()/AllowAnyHeader()) — sprawdź w Narzędziach deweloperskich przeglądarki, jakie DOKŁADNIE nagłówki/metody wysyła Angular, i skonfiguruj politykę CORS możliwie RESTRYKCYJNIE, ograniczając ją tylko do faktycznie potrzebnych wartości.

Typowe błędy

Brak konfiguracji CORS albo zła kolejność UseCors/MapControllers — najczęstszy błąd na start tej pary technologii; objawia się błędem "blocked by CORS policy" w konsoli przeglądarki, mimo że bezpośrednie wywołanie API przez Postmana (bez CORS, bo to nie przeglądarka) działa poprawnie.

Zapominanie o subskrypcji (.subscribe()) po wywołaniu metody serwisu zwracającej Observable — bez subskrypcji żądanie HTTP W OGÓLE się nie wykonuje (Observable jest "leniwy" — nic się nie dzieje, dopóki ktoś się nie zasubskrybuje).

Wpisanie na sztywno pełnego adresu backendu w KAŻDYM serwisie zamiast trzymania go w jednym, centralnym miejscu konfiguracji (np. pliku environment.ts) — utrudnia to później zmianę adresu przy wdrożeniu na serwer produkcyjny (lekcja 18).

Nawiązanie do egzaminu zawodowego

To realizacja mostu między frontendem (Angular, lekcje 9-11) a backendem (ASP.NET Core) wymaganego przez INF.04.7.2-3 — dokładnie ta architektura (SPA + REST API), którą informator CKE opisuje jako przykładowe zadanie praktyczne: "implementacja aplikacji internetowych przy wykorzystaniu Angular... i implementacja części serwerowej w środowisku... ASP.NET". Kolejne lekcje (12-17) rozwijają backend o sesje, logowanie, bazę danych i pliki — wszystkie w tej samej roli: dostarczyciela danych dla Angulara.