cs

Tworzenie prostych aplikacji używających bazy danych przy użyciu technologii Entity Framework.

wersja 1

https://learn.microsoft.com/en-us/ef/core/cli/dotnet https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding

Konfiguracja połączenia z bazą danych

Konfiguracja połączenia z bazą danych w Entity Framework Core opiera się na współpracy kilku kluczowych elementów projektu. Najpierw, w pliku appsettings.json, umieszcza się ciąg połączenia (connection string), który zawiera wszystkie informacje potrzebne do nawiązania komunikacji z bazą danych, takie jak adres serwera, nazwa bazy, dane logowania oraz dodatkowe parametry konfiguracyjne. Ten plik powinien znajdować się w głównym katalogu projektu (obok pliku Program.cs) i jest automatycznie odczytywany podczas uruchamiania aplikacji.

Konfiguracja może być przechowywana w domyślnych plikach i źródłach, które są odczytywane w następującej kolejności:

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. User Secrets (tylko w Development)
  4. Zmienne środowiskowe
  5. Argumenty wiersza poleceń

Jeśli poszczególne źródła konfiguracji modyfikują ten sam parametr, zostanie użyta wartość z tego źródła, które zostanie wczytane najpóźniej – czyli ma wyższy priorytet.

Przykładowy plik appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=YourDatabaseName;Trusted_Connection=True;"
  }
}

Kolejnym krokiem jest utworzenie klasy kontekstu, dziedziczącej po DbContext. Klasa ta, zwykle nazywana AppDbContext lub podobnie, znajduje się zazwyczaj w folderze Data lub Infrastructure, aby zachować porządek w projekcie. W tej klasie definiuje się właściwości typu DbSet, które reprezentują tabele w bazie danych, a także (opcjonalnie) metodę OnModelCreating, jeśli chcemy konfigurować modele za pomocą Fluent API.

using Microsoft.EntityFrameworkCore;
using YourProject.Models;

namespace YourProject.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {}

        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }

        // (opcjonalnie) konfiguracje modeli przez Fluent API
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }
}

Aby połączyć te elementy, w pliku Program.cs (lub Startup.cs w starszych wersjach .NET) należy zarejestrować kontekst w kontenerze zależności za pomocą builder.Services.AddDbContext<>(), przekazując do niego odpowiednią metodę dostawcy bazy danych, np. UseSqlServer() w przypadku SQL Servera. W tej metodzie wykorzystuje się wcześniej zdefiniowany ciąg połączenia odczytany przez Configuration.GetConnectionString(“nazwa”). Aby narzędzia wiersza poleceń EF Core (dotnet ef migrations, dotnet ef database update) działały poprawnie w aplikacjach nie opartych o ASP.NET Core (jak Windows Forms, WPF itp.), należy utworzyć fabrykę kontekstu (Design-Time DbContext Factory). Dzięki niej EF Core wie, jak utworzyć kontekst podczas operacji projektowych.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using System.IO;

// Fabryka kontekstu używana podczas operacji projektowych (np. migracji).
// Dzięki niej Entity Framework Core wie, jak utworzyć DbContext,
// nawet gdy aplikacja nie korzysta z ASP.NET Core (np. Windows Forms).
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
    // Metoda wywoływana przez EF Core w czasie projektowym (design-time),
    // np. podczas wykonywania poleceń migracji.
    public AppDbContext CreateDbContext(string[] args)
    {
        // Tworzenie konfiguracji na podstawie pliku appsettings.json.
        // Directory.GetCurrentDirectory() zapewnia poprawną ścieżkę podczas działania z CLI.
        var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory()) // Ustawienie katalogu bazowego (ważne przy uruchamianiu z linii komend)
            .AddJsonFile("appsettings.json")              // Dodanie pliku z ustawieniami
            .Build();                                     // Zbudowanie obiektu konfiguracji

        // Tworzymy obiekt opcji dla DbContext na podstawie connection stringa.
        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        var connectionString = configuration.GetConnectionString("DefaultConnection");

        // Konfigurujemy DbContext do używania SQL Servera (można zmienić na np. UseSqlite).
        optionsBuilder.UseSqlServer(connectionString);

        // Zwracamy nową instancję kontekstu z odpowiednio skonfigurowanymi opcjami.
        return new AppDbContext(optionsBuilder.Options);
    }
}

Cały ten proces zapewnia modularność i elastyczność – pozwala zmieniać bazę danych bez ingerencji w logikę aplikacji, a dzięki wykorzystaniu dependency injection kontekst EF jest bezproblemowo dostępny w kontrolerach, serwisach czy repozytoriach. Dodatkowo, jeśli projekt wykorzystuje migracje, po zakończeniu konfiguracji można wygodnie zarządzać strukturą bazy komendami dotnet ef migrations add i dotnet ef database update.

Klasy modelowe

Klasa modelowa w Entity Framework to zwykła klasa C#, która odwzorowuje strukturę tabeli w bazie danych – jej właściwości odpowiadają kolumnom, a relacje między klasami odzwierciedlają powiązania między tabelami (np. jeden-do-wielu). Klasy modelowe powinny być umieszczane w folderze Models, w osobnych plikach nazwanych zgodnie z nazwą klasy, czyli w liczbie pojedynczej i z użyciem notacji PascalCase (np. Product.cs, Customer.cs). Dzięki temu projekt jest czytelny, łatwy do utrzymania i zgodny z przyjętymi konwencjami. Modele te są następnie rejestrowane w klasie kontekstu DbContext, gdzie każda z nich powinna mieć przypisany DbSet<T>, co umożliwia EF automatyczne tworzenie i obsługę odpowiadających im tabel. W razie potrzeby klasy można dodatkowo opisywać adnotacjami, takimi jak [Key], [Required], [MaxLength], czy [ForeignKey], aby precyzyjnie kontrolować mapowanie do bazy danych i walidację danych na poziomie modelu.

Wybrane adnoracje

Adnotacja Co robi Użycie (gdzie stosować)
[Key] Ustawia pole jako klucz główny (PRIMARY KEY) Właściwość
[Required] Pole nie może być NULL Właściwość
[MaxLength(n)] Maksymalna długość ciągu znaków (np. dla nvarchar(n)) Właściwość
[MinLength(n)] Minimalna długość ciągu znaków (głównie walidacja w ASP.NET) Właściwość
[StringLength(max)] Określa maksymalną i opcjonalnie minimalną długość Właściwość
[Range(min, max)] Ogranicza wartość liczbową do danego zakresu Właściwość
[RegularExpression("regex")] Waliduje tekst zgodnie z wyrażeniem regularnym Właściwość
[Column("Nazwa", TypeName = "typ_sql")] Ustawia nazwę i typ kolumny w bazie danych (np. nvarchar(100)) Właściwość
[Table("NazwaTabeli")] Ustawia nazwę tabeli (gdy inna niż nazwa klasy) Klasa
[NotMapped] Właściwość nie zostanie zmapowana do bazy danych Właściwość
[DatabaseGenerated(DatabaseGeneratedOption.None)] Użytkownik musi sam wypełnić pole Właściwość
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] Wartość generowana automatycznie (np. AUTO_INCREMENT) Właściwość
[DatabaseGenerated(DatabaseGeneratedOption.Computed)] Generowane przez SQL (np. GETDATE()) Właściwość
[ForeignKey("NazwaWłaściwości")] Definiuje właściwość jako klucz obcy Właściwość
[InverseProperty("WłaściwośćZDrugiejStrony")] Określa odwrotną nawigację w relacji (np. 1:N) Właściwość
[DataType(DataType.X)] Ustawia typ danych (pomocne w formularzach: Email, Phone, Date, Url itd.) Właściwość
[EmailAddress] Waliduje poprawność adresu e-mail Właściwość
[Phone] Waliduje numer telefonu Właściwość
[Url] Waliduje URL Właściwość
[Display(Name = "NazwaWyświetlana")] Zmienia etykietę w formularzach i widokach Właściwość
[DefaultValue("SQL_Wyrażenie")] Wartość domyślna kolumny (np. GETDATE()) – działa głównie z Fluent API Właściwość
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace YourProject.Models
{
    public class Product
    {
        [Key] // Klucz główny
        public int Id { get; set; }

        [Required] // Wymagane pole
        [MaxLength(100)] // Maksymalna długość tekstu
        public string Name { get; set; }

        [Column(TypeName = "decimal(10,2)")] // Typ danych w bazie
        public decimal Price { get; set; }

        [DataType(DataType.Date)]
        public DateTime CreatedAt { get; set; } = DateTime.Now;

        // Relacja do innej tabeli (wielu do jednego)
        public int CategoryId { get; set; }

        [ForeignKey("CategoryId")]
        public Category Category { get; set; }
    }
}

Migracje

Operacja na migracjach

Wyświetlenie listy migraacji

dotnet ef migrations list

Dodanie pierwszej migracji na podstawie klas modelowych

dotnet ef migrations add Init

Dodanie migracji o danej nazwie

dotnet ef migrations add <Migration_name>
public partial class AddNewTablesAndIndexes : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Tworzenie nowej tabeli 'Categories'
        migrationBuilder.CreateTable(
            name: "Categories", // Nazwa tabeli
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"), // Klucz główny z autoinkrementacją
                Name = table.Column<string>(nullable: false) // Kolumna 'Name' nie może być null
            },
            constraints: table =>
            {
                // Określenie klucza głównego w tabeli 'Categories'
                table.PrimaryKey("PK_Categories", x => x.Id); // Ustawienie 'Id' jako klucz główny
            });

        // Tworzenie nowej tabeli 'Customers'
        migrationBuilder.CreateTable(
            name: "Customers", // Nazwa tabeli
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"), // Klucz główny z autoinkrementacją
                FullName = table.Column<string>(nullable: false), // Kolumna 'FullName' nie może być null
                Email = table.Column<string>(nullable: true) // Kolumna 'Email' może być null
            },
            constraints: table =>
            {
                // Określenie klucza głównego w tabeli 'Customers'
                table.PrimaryKey("PK_Customers", x => x.Id); // Ustawienie 'Id' jako klucz główny
            });

        // Dodanie nowej kolumny 'CategoryId' do tabeli 'Products'
        // Kolumna ta będzie wskazywała na tabelę 'Categories' (relacja z kluczem obcym)
        migrationBuilder.AddColumn<int>(
            name: "CategoryId", // Nazwa kolumny
            table: "Products", // Tabela, do której dodajemy kolumnę
            nullable: true); // Kolumna może mieć wartość null (może nie mieć przypisanej kategorii)

        // Tworzenie indeksu na kolumnie 'CategoryId' w tabeli 'Products'
        // Indeks przyspiesza operacje wyszukiwania, sortowania i filtracji po tej kolumnie
        migrationBuilder.CreateIndex(
            name: "IX_Products_CategoryId",  // Nazwa indeksu
            table: "Products",            // Tabela, na której tworzymy indeks
            column: "CategoryId");        // Kolumna, dla której tworzymy indeks

        // Tworzenie klucza obcego między tabelą 'Products' a tabelą 'Categories'
        // Zapewnia to powiązanie produktu z kategorią, w której jest przypisany
        migrationBuilder.AddForeignKey(
            name: "FK_Products_Categories_CategoryId", // Nazwa klucza obcego
            table: "Products",                      // Tabela, do której dodajemy klucz obcy
            column: "CategoryId",                   // Kolumna, która będzie kluczem obcym
            principalTable: "Categories",           // Tabela, do której klucz obcy się odnosi
            principalColumn: "Id",                  // Kolumna, do której klucz obcy się odnosi (klucz główny)
            onDelete: ReferentialAction.SetNull);  // Zachowanie przy usuwaniu - usunięcie kategorii ustawi null w CategoryId

    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        // Usuwanie klucza obcego z tabeli 'Products'
        // Zdejmuje powiązanie między produktem a kategorią
        migrationBuilder.DropForeignKey(
            name: "FK_Products_Categories_CategoryId",
            table: "Products");

        // Usuwanie indeksu z tabeli 'Products' na kolumnie 'CategoryId'
        // Zwalnia miejsce, usuwając indeks na tej kolumnie
        migrationBuilder.DropIndex(
            name: "IX_Products_CategoryId",
            table: "Products");

        // Usuwanie kolumny 'CategoryId' z tabeli 'Products'
        // Kolumna ta była kluczem obcym wskazującym na tabelę 'Categories'
        migrationBuilder.DropColumn(
            name: "CategoryId",
            table: "Products");

        // Usuwanie tabeli 'Customers'
        // Całkowite usunięcie tabeli zawierającej dane klientów
        migrationBuilder.DropTable(
            name: "Customers");

        // Usuwanie tabeli 'Categories'
        // Całkowite usunięcie tabeli zawierającej dane kategorii
        migrationBuilder.DropTable(
            name: "Categories");
    }
}

Usunięcie migracji

dotnet ef migrations remove

Operacje na bazie danych

Usuwa bazę danych

dotnet ef database drop

Wykonanie migracji (wywołanie metody up)

dotnet ef database update <Nazwa migracji>

Cofanie wszystkich migracji (wywołanie metody down)

dotnet ef database update 0

CRUD

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

public class UserService
{
    private readonly AppDbContext _db;

    public UserService(AppDbContext db)
    {
        _db = db;
    }

    // CREATE
    public async Task CreateUserAsync(User user)
    {
        _db.Users.Add(user);
        await _db.SaveChangesAsync();
    }

    // READ ALL
    public async Task<List<User>> GetAllUsersAsync()
    {
        return await _db.Users.ToListAsync();
    }

    // READ BY ID
    public async Task<User> GetUserByIdAsync(int id)
    {
        return await _db.Users.FindAsync(id);
    }

    // READ CHUNKS (np. do paginacji)
    public async Task<List<User>> GetUsersPagedAsync(int pageNumber, int pageSize)
    {
        return await _db.Users
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
    }

    // FILTERING (np. użytkownicy powyżej 30 roku życia)
    public async Task<List<User>> GetUsersFilteredAsync(int minAge)
    {
        return await _db.Users
            .Where(u => u.Age >= minAge)
            .ToListAsync();
    }

    // SORTING (np. po imieniu rosnąco)
    public async Task<List<User>> GetUsersSortedByNameAsync()
    {
        return await _db.Users
            .OrderBy(u => u.Name)
            .ToListAsync();
    }

    // UPDATE
    public async Task UpdateUserAsync(User updatedUser)
    {
        var existing = await _db.Users.FindAsync(updatedUser.Id);
        if (existing == null) return;

        existing.Name = updatedUser.Name;
        existing.Email = updatedUser.Email;
        existing.Age = updatedUser.Age;

        await _db.SaveChangesAsync();
    }

    // DELETE
    public async Task DeleteUserAsync(int id)
    {
        var user = await _db.Users.FindAsync(id);
        if (user != null)
        {
            _db.Users.Remove(user);
            await _db.SaveChangesAsync();
        }
    }
}

Typy komponentów UI w Windows Forms

Element Czym jest? Kiedy używać?
Form Okno aplikacji Każdy osobny widok, np. MainForm, LoginForm, ProductsView
MDI Parent Form Główne okno, które “hostuje” inne formularze (MdiChildren) Gdy chcesz jedno okno z dynamicznie ładowanymi widokami
User Control Część UI, którą możesz wielokrotnie osadzać w formularzach Gdy masz komponent wielokrotnego użycia, np. ProductCard, AddressPanel
Custom Control Kontrolka napisana od zera lub rozszerzona (np. własny przycisk) Gdy potrzebujesz bardziej zaawansowaną, własną kontrolkę, np. RoundedButton, StarRating
AboutBox Wbudowany szablon formularza “O programie” (z nazwą, wersją, itd.) Tylko do wyświetlania informacji o programie (wersja, autor itp.)

Zadania

Zadanie 1

Utwórz projekt Windows Forms w Visual Studio. Użyj NuGet Package Manager, aby zainstalować Microsoft.EntityFrameworkCore oraz Microsoft.EntityFrameworkCore.SqlServer (lub inny provider np. Microsoft.EntityFrameworkCore.Sqlite). Zaprojektuj model danych: Utwórz klasy modelu User, Product, Order i dodaj odpowiednie atrybuty ([Key], [Required], [MaxLength] itd.). Utwórz klasę AppDbContext, która dziedziczy po DbContext, i dodaj DbSet dla swoich modeli. Skonfiguruj połączenie z bazą danych w App.config (lub appsettings.json) i utwórz connection string.

Zadanie 2

W terminalu użyj komendy dotnet ef migrations add InitialCreate, aby stworzyć migrację na podstawie swoich modeli. Użyj komendy dotnet ef database update, aby utworzyć tabele w bazie danych. Zaktualizuj modele (np. dodaj nowe właściwości) i ponownie utwórz migrację oraz zaktualizuj bazę danych. (Zrozum różnicę między Add-Migration a Update-Database – Kiedy należy ich używać?)

Zadanie 3

Stwórz formularz w Windows Forms do dodawania użytkowników do bazy. Wyświetl listę użytkowników w DataGridView. Dodaj możliwość edytowania i usuwania użytkowników z bazy danych.

Zadanie 4

Utwórz modele Order i połącz je z User (relacja 1:N). Zaktualizuj bazę danych po dodaniu nowych modeli. Utwórz formularz do wyświetlania zamówień danego użytkownika.

Zadanie 5

Użyj biblioteki Bogus do generowania przykładowych danych użytkowników i zamówień. Dodaj metodę seedującą, która dodaje dane do bazy przy starcie aplikacji.