CRUD w ASP.NET Core — jak dodać obsługę bazy w MVC

Pełny cykl Create/Read/Update/Delete w ASP.NET Core MVC z Entity Framework. Akcje w controllerze, async/await, SaveChangesAsync i formularz DELETE krok po kroku.

JAJarosław Armatys 8 min czytania
Wersja wideo tego wpisu — obejrzyj na YouTube

Dotarliśmy do siódmego odcinka kursu ASP.NET Core. Poprzednio spięliśmy aplikację z Entity Framework i dodaliśmy pierwsze wpisy do bazy. Teraz czas na coś, co w codziennej pracy .NET developera pojawia się praktycznie zawsze — CRUD. Cztery proste operacje, bez których żadna aplikacja biznesowa nie istnieje.

Co to CRUD i dlaczego 80% aplikacji to właśnie to

CRUD to skrót od Create, Read, Update, Delete — czyli dodaj, odczytaj, zaktualizuj i usuń. Brzmi banalnie, ale jak się dobrze przyjrzeć typowej aplikacji webowej, to okaże się, że większość funkcjonalności to po prostu lepiej ubrany CRUD. Panel admina? CRUD. Koszyk w sklepie? CRUD. Lista zadań? CRUD.

Jeżeli opanujesz CRUD w ASP.NET Core MVC z Entity Framework, to masz fundament pod każdą aplikację biznesową, którą przyjdzie ci napisać. Reszta to już tylko warianty na ten sam temat.

W tej lekcji rozszerzamy WarehouseService z poprzedniego odcinka o brakujące metody, dokładamy widoki i przyciski w MVC, a całość pokazuję na realnym przykładzie magazynu produktów. Kod do pobrania leży na GitHubie: jarmatys/asp-net-core-podstawy · Lekcja-7.

Architektura — kontroler cienki jak papier

Zanim wejdziemy w kod, warto powiedzieć jedno — kontroler w MVC ma być cienki. Jego robota to:

  1. Przyjąć request.
  2. Zawołać serwis.
  3. Wrzucić dane do widoku (lub przekierować).

Tyle. Cała logika biznesowa, odwołania do bazy, transformacje danych — to nie jest miejsce dla kontrolera. To idzie do serwisu (WarehouseService), który kontroler wstrzykuje przez DI.

Dzięki takiemu podziałowi kontroler wygląda czysto, serwis jest testowalny, a jak pewnego dnia będziesz chciał wystawić to samo jako API zamiast MVC — to wymienisz tylko controller, a serwis zostawisz bez zmian.

Read — listowanie i szczegóły

Zaczynamy od odczytu, bo to najprostsza część. W WarehouseController mamy akcję Index, która zwraca listę produktów, oraz Details, która przyjmuje id i zwraca pojedynczy rekord.

public class WarehouseController : Controller
{
    private readonly IWarehouseService _warehouseService;

    public WarehouseController(IWarehouseService warehouseService)
    {
        _warehouseService = warehouseService;
    }

    public async Task<IActionResult> Index()
    {
        var products = await _warehouseService.GetProductsAsync();
        return View(products);
    }

    public async Task<IActionResult> Details(int id)
    {
        var product = await _warehouseService.GetProductAsync(id);
        return View(product);
    }
}

W serwisie metoda GetProductAsync używa FindAsync z DbContext — to najszybszy sposób na wyciągnięcie pojedynczego rekordu po kluczu głównym, bo EF najpierw sprawdza cache zmian, a dopiero potem wali zapytaniem do bazy.

public async Task<Product> GetProductAsync(int id)
{
    return await _context.Products.FindAsync(id);
}

Widok Index.cshtml dostaje silnie typowaną listę (@model IEnumerable<Product>), a Details.cshtml pojedynczy obiekt (@model Product). Na liście renderuję nazwę, kategorię i dwa linki — do szczegółów i do usunięcia.

Create — dodawanie wiersza

Dodawanie produktu to klasyczny schemat GET + POST. Akcja GET zwraca pustą formę, akcja POST przyjmuje model, waliduje go i zapisuje do bazy.

[HttpGet]
public IActionResult Create() => View();

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
    if (!ModelState.IsValid) return View(product);
    _context.Products.Add(product);
    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}

Kluczowe rzeczy w tym snippecie:

  • [ValidateAntiForgeryToken] — chroni przed CSRF. W widoku Html.BeginForm automatycznie wstawia ukryty token.
  • ModelState.IsValid — sprawdza walidację na podstawie atrybutów ([Required], [StringLength] itd.) w modelu. Jeśli coś nie gra, wracamy do formy z błędami.
  • _context.Products.Add — nie trafia od razu do bazy. Ustawia tylko stan encji na Added w changetrackerze EF.
  • await _context.SaveChangesAsync() — dopiero tu leci prawdziwy INSERT do SQL. Wersja async nie blokuje wątku IIS na czas I/O.
  • RedirectToAction(nameof(Index)) — po zapisie robimy PRG pattern (Post-Redirect-Get), żeby przy F5 nie duplikować rekordu.

Zauważ, że nigdzie nie piszę ręcznie SQL. EF Core robi to za mnie — a jak zapisuję encję z flagą Added, to wygeneruje INSERT INTO Products (...) VALUES (...). Mniej kodu, mniej miejsc do pomyłek.

Update i Delete

Update wygląda prawie jak Create — GET wyciąga istniejący rekord, POST przyjmuje zmodyfikowany model i zapisuje.

[HttpGet]
public async Task<IActionResult> Edit(int id)
{
    var product = await _warehouseService.GetProductAsync(id);
    return View(product);
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Product product)
{
    if (!ModelState.IsValid) return View(product);
    _context.Products.Update(product);
    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}

Różnica jest w jednej linijce — zamiast Add używam Update, co ustawia stan encji na Modified. SaveChangesAsync wygeneruje wtedy UPDATE zamiast INSERT.

Delete ma jeden haczyk. W MVC standardowo robimy delete przez formularz POST, bo zwykły link HTTP-owo byłby GET-em — a usuwanie danych przez GET to prosta droga do sytuacji, w której crawler Google albo prefetcher przeglądarki skasuje ci pół bazy. Dlatego w widoku listy każdy rekord ma mini-formularz z przyciskiem:

@using (Html.BeginForm("Delete", "Warehouse", FormMethod.Post))
{
    <input type="hidden" name="id" value="@product.Id" />
    <button type="submit">Usuń</button>
}

A w kontrolerze:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
    await _warehouseService.DeleteProductAsync(id);
    return RedirectToAction(nameof(Index));
}

I metoda w serwisie:

public async Task DeleteProductAsync(int id)
{
    var product = await _context.Products.FindAsync(id);
    _context.Products.Remove(product);
    await _context.SaveChangesAsync();
}

Tu jedna ważna rzecz, która w materiale filmowym wywaliła błąd na żywo — atrybut name w inpucie musi się zgadzać z nazwą parametru w akcji kontrolera. Jak w akcji masz Delete(int id), to input musi mieć name="id". MVC po nazwie mapuje pola formularza na parametry — pomylisz literkę i dostajesz id = 0.

To akurat klasyczny błąd początkującego. BeginForm wygląda jak magia, ale pod spodem to zwykłe name/value pary, które framework parsuje. Jak coś nie działa — otwórz DevTools, zobacz co faktycznie leci w body requestu.

Async/await — dlaczego nie wystarczy zwykły SaveChanges

We wszystkich metodach używam async/await i wersji async metod EF-a (FindAsync, SaveChangesAsync). To nie jest dla ozdoby. ASP.NET Core ma ograniczoną pulę wątków, które obsługują requesty. Jak wywołasz synchroniczne SaveChanges, to wątek stoi zablokowany przez cały czas zapisu do bazy — nawet jak to jest 50 ms, to przy 200 jednoczesnych requestach masz 200 zablokowanych wątków.

Async zwalnia wątek na czas oczekiwania na bazę. Framework może w tym czasie obsłużyć inne requesty, a jak baza odpowie — wraca do naszego kodu. W praktyce aplikacja skaluje się znacznie lepiej, a kosztem są trzy literki Async w nazwie metody.

Co dalej?

CRUD masz ogarnięty. W następnym odcinku bierzemy się za coś, co nadaje aplikacji charakter produkcyjny — autoryzację użytkowników. Logowanie, rejestracja, zabezpieczenie endpointów przed niezalogowanymi. ASP.NET Core ma to w standardzie (Identity), więc pokażę, jak to wpiąć bez pisania wszystkiego od zera.

Cały kod z tej lekcji znajdziesz na branchu Lekcja-7: github.com/jarmatys/asp-net-core-podstawy/tree/Lekcja-7. Sklonuj, odpal, pobaw się — a jak coś nie działa albo masz pytanie, to walnij w komentarz pod filmem. Do zobaczenia w kolejnym odcinku.

Tagi:#course-aspnet#dotnet#crud#entity-framework

Masz pytanie do tego wpisu?

Napisz do mnie — chętnie porozmawiam o szczegółach technicznych, albo o tym jak zastosować to u Ciebie w projekcie.