ASP.NET Core Web API: kontroleri i CRUD operacije

Lekcija 3 od 7 · ASP.NET Core

Pravimo pravi REST API za projekat Prodavnica: model Proizvod i kontroler sa operacijama čitanja, dodavanja, izmene i brisanja (CRUD). Podaci su za sada u memoriji, a u sledećoj lekciji prelaze u bazu.

Pre nego što počneš: potrebno predznanje – statičke strane i servisi, klase i liste u C#-u.
Kako da učiš: radi u svom projektu korak po korak i posle svake izmene pokreni aplikaciju; zadatak na kraju prvo uradi sam/a. Kod je u C#-u za .NET 8 ili noviji (preporuka: aktuelna LTS verzija); naziv fajla je u komentaru na vrhu svakog primera.

Novi projekat

dotnet new webapi --use-controllers -o Prodavnica
cd Prodavnica
:: šablon sadrži primer WeatherForecast – obriši Controllers/WeatherForecastController.cs i WeatherForecast.cs
// Program.cs – Web API sa kontrolerima (lekcija 3)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();          // registruje kontrolere

var app = builder.Build();
app.UseHttpsRedirection();
app.MapControllers();                       // rute iz atributa [Route], [HttpGet]...
app.Run();

Šablon u Program.cs može imati i podešavanja za OpenAPI/Swagger (opis API-ja). Mogu da ostanu – ovde ih ne koristimo, jer API proveravamo .http fajlom.

Model: klasa Proizvod

// Models/Proizvod.cs
using System.ComponentModel.DataAnnotations;

namespace Prodavnica.Models;

public class Proizvod
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Naziv je obavezan.")]
    [StringLength(50)]
    public string Naziv { get; set; } = "";

    [Range(0, 1_000_000, ErrorMessage = "Cena mora biti od 0 do 1.000.000 dinara.")]
    public int Cena { get; set; }            // u dinarima (ceo broj – jednostavno i za SQLite)

    [Range(0, 10_000)]
    public int Kolicina { get; set; }
}

Atributi [Required], [StringLength] i [Range] su pravila provere. Zbog atributa [ApiController] na kontroleru, neispravan zahtev automatski dobija odgovor 400 Bad Request sa opisom grešaka – ne pišemo nijedan if. Cena je ceo broj dinara, da bi kasnije rad sa bazom SQLite bio jednostavan.

Kontroler

// Controllers/ProizvodiController.cs – podaci u memoriji (lekcija 3)
using Microsoft.AspNetCore.Mvc;
using Prodavnica.Models;

namespace Prodavnica.Controllers;

[ApiController]                       // automatska provera modela, odgovori 400 sa greškama
[Route("api/[controller]")]           // [controller] = "Proizvodi" → /api/proizvodi
public class ProizvodiController : ControllerBase
{
    // static: lista živi dok radi server (posle ponovnog pokretanja vraća se na početak)
    private static readonly List<Proizvod> proizvodi =
    [
        new Proizvod { Id = 1, Naziv = "Sveska", Cena = 120, Kolicina = 50 },
        new Proizvod { Id = 2, Naziv = "Olovka", Cena = 60, Kolicina = 200 },
        new Proizvod { Id = 3, Naziv = "Lenjir", Cena = 150, Kolicina = 30 },
    ];
    private static int sledeciId = 4;

    // GET /api/proizvodi
    [HttpGet]
    public IEnumerable<Proizvod> Svi() => proizvodi;

    // GET /api/proizvodi/2
    [HttpGet("{id:int}")]
    public ActionResult<Proizvod> Jedan(int id)
    {
        var p = proizvodi.FirstOrDefault(x => x.Id == id);
        if (p == null) return NotFound();                 // 404
        return p;                                         // 200 + JSON
    }

    // POST /api/proizvodi   (telo: JSON novog proizvoda)
    [HttpPost]
    public ActionResult<Proizvod> Dodaj(Proizvod novi)
    {
        novi.Id = sledeciId++;                            // id određuje server
        proizvodi.Add(novi);
        return CreatedAtAction(nameof(Jedan), new { id = novi.Id }, novi);   // 201 + adresa novog
    }

    // PUT /api/proizvodi/2
    [HttpPut("{id:int}")]
    public IActionResult Izmeni(int id, Proizvod izmenjen)
    {
        var p = proizvodi.FirstOrDefault(x => x.Id == id);
        if (p == null) return NotFound();
        p.Naziv = izmenjen.Naziv;
        p.Cena = izmenjen.Cena;
        p.Kolicina = izmenjen.Kolicina;
        return NoContent();                               // 204
    }

    // DELETE /api/proizvodi/2
    [HttpDelete("{id:int}")]
    public IActionResult Obrisi(int id)
    {
        var p = proizvodi.FirstOrDefault(x => x.Id == id);
        if (p == null) return NotFound();
        proizvodi.Remove(p);
        return NoContent();
    }
}
Metoda HTTPAdresaAkcijaUspešan odgovor
GET/api/proizvodiSvi200 + lista
GET/api/proizvodi/2Jedan200 + proizvod (ili 404)
POST/api/proizvodiDodaj201 Created + adresa novog proizvoda u zaglavlju Location
PUT/api/proizvodi/2Izmeni204 No Content
DELETE/api/proizvodi/2Obrisi204 No Content

{id:int} je ograničenje rute: /api/proizvodi/abc neće ni pozvati akciju. Statusni kodovi su isti kao u tabeli iz lekcije HTTP protokol.

Provera API-ja: .http fajl

### Prodavnica.http – zahtevi za proveru API-ja (VS Code: dodatak REST Client; Visual Studio: ugrađeno)
@host = http://localhost:5123

GET {{host}}/api/proizvodi

###
GET {{host}}/api/proizvodi/2

###
POST {{host}}/api/proizvodi
Content-Type: application/json

{ "naziv": "Gumica", "cena": 40, "kolicina": 80 }

###
PUT {{host}}/api/proizvodi/2
Content-Type: application/json

{ "naziv": "Olovka HB", "cena": 70, "kolicina": 190 }

###
DELETE {{host}}/api/proizvodi/3

###
POST {{host}}/api/proizvodi
Content-Type: application/json

{ "naziv": "", "cena": -5, "kolicina": 1 }

U VS Code-u instaliraj dodatak REST Client, otvori fajl i klikni Send Request iznad zahteva; Visual Studio to ima ugrađeno. Poslednji zahtev namerno šalje neispravne podatke – odgovor 400 sadrži poruke sa atributa iz modela. Isto možeš i iz komandne linije: curl http://localhost:5123/api/proizvodi.

Kontroleri i MVC

Kontroler je deo obrasca MVC (Model – View – Controller): model opisuje podatke, kontroler prima zahteve, a view (pogled) prikazuje rezultat. Web API kontroler (ControllerBase) nema poglede – vraća JSON, a prikaz prepušta klijentu (lekcija 5). Kontroler sa HTML pogledima (Controller) je u lekciji 6.

Zadaci za vežbu

Za svaki zadatak postoji primer, uputstvo i rešenje.

ZADATAK 01Težina: srednje

Filtriranje po ceni

Dodaj akciju GET /api/proizvodi/jeftini?max=100 koja vraća proizvode čija cena nije veća od max, sortirane po ceni, i GET /api/proizvodi/ukupno koja vraća ukupnu vrednost robe (zbir cena × količina).

Česte greške i saveti
  • Ime klase kontrolera mora se završavati sa Controller; deo ispred je vrednost [controller] u ruti.
  • Bez builder.Services.AddControllers() i app.MapControllers() kontroleri se ne vide (404 za sve adrese).
  • Podaci u statičkoj listi nestaju pri ponovnom pokretanju – zato sledeća lekcija uvodi bazu.

Proveri znanje

Otvori kviz

Web API kontroleri

1. Koji statusni kod vraća uspešan POST koji pravi novi proizvod?




2. Šta radi atribut [ApiController] kada stigne neispravan model?




3. Na koju adresu odgovara kontroler ProizvodiController sa [Route("api/[controller]")]?