Lekcja 12. ASP.NET Core jako backend dla Angular — Web API i CORS
Trudny / egzaminacyjnyPo 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
Dodaj do
ProduktyControllerakcję[HttpDelete("{id}")]usuwającą produkt, i odpowiadającą jej metodęusun(id: number)wProduktService.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.
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.