SQLite

Prof. Dr. Robin Nunkesser

SQLite

Beispielprojekt

Vorgehen

Agent-generated example: csharp-maui-sqlite

  • Ziel: ein kleines, gut erklärbares Beispiel für SQLite in MAUI
  • sichtbares Ergebnis: Todo-Liste mit Hinzufügen, Abhaken und Löschen, persistent in einer lokalen SQLite-Datenbank
  • ursprünglich out of scope: MVVM, DI, Repository-Pattern — nach Refaktorierung jetzt enthalten
  • weiterhin out of scope: Migrations, mehrere Tabellen

Verwendete Instructions

  • System- und Agent-Regeln: kleinster sinnvoller Scope, erst Kontext lesen, Änderungen verifizieren
  • .github/copilot-instructions.md: kleine, reviewbare Änderungen und echte Verifikation
  • .github/instructions/artifacts.instructions.md: kleine, explizite, verifizierbare Änderungen in artifacts/
  • .github/instructions/artifacts-consumers-education.instructions.md: didaktische Klarheit, ein Hauptkonzept, Foliensatzbezug und Vergleich mit dem manuellen Beispiel
  • .github/instructions/slides.instructions.md: knappe, gut scannbare Folien

Exakter Prompt

Erstelle ein kleines agent-generated example für SQLite in einem Lehrprojekt dieses Repos.
Lies zuerst die relevanten Repository-Instructions und ermittle den zugehörigen Foliensatz.
Die Folien sind in md/quarto/ae/SQLite.qmd leer vorbereitet. Das bisherige Beispiel ist in
artifacts/consumers/education/csharp-recipes/SQLiteRecipe, stark veraltet und noch nicht in
einem eigenen Repository. Als Ziel-Repo habe ich
https://github.com/RobinNunkesser/csharp-maui-sqlite.git angelegt.
Dokumentiere die verwendeten Instructions und den exakten Prompt, füge beides in den
Foliensatz ein und vergleiche das Ergebnis mit dem bestehenden manuellen Beispiel.
Bevorzuge didaktische Klarheit, kleinen Scope und überprüfbare Ergebnisse.

Ergebnis

Lernziel

  • sqlite-net-pcl in einer MAUI-App einbinden
  • Ports-&-Adapters-Architektur mit 4 Projekten (*.Core, *.Infrastructure, *.App, *.Tests)
  • MVVM: ObservableObject, [ObservableProperty], [RelayCommand] mit CommunityToolkit.Mvvm
  • Repository-Interface in Core, SQLite-Adapter in Infrastructure
  • Entitäts-Mapping: TodoItemEntity (Infrastructure) ↔︎ TodoItem (Core)
  • Dependency Injection in MauiProgram.cs
  • Unit Tests mit xUnit und FakeTodoRepository

Projektstruktur

csharp-maui-sqlite/
├── SQLite.Core/           # Domain + Interface + ViewModel
├── SQLite.Infrastructure/ # sqlite-net-pcl Adapter
├── SQLite/                # MAUI App (Composition Root)
└── SQLite.Tests/          # xUnit (nur Core)
  • SQLite.Core kennt keine Persistenz-Technologie.
  • SQLite.Infrastructure kennt sqlite-net-pcl, aber nicht MAUI.

Core: TodoItem

public partial class TodoItem : ObservableObject
{
    public int Id { get; set; }

    [ObservableProperty]
    private string _title = string.Empty;

    [ObservableProperty]
    private bool _done;
}
  • ObservableObject + [ObservableProperty] generieren INotifyPropertyChanged.
  • Kein [PrimaryKey]-Attribut: das gehört zur Infrastructure.

Core: ITodoRepository

public interface ITodoRepository
{
    Task<List<TodoItem>> GetAllAsync();
    Task SaveAsync(TodoItem item);
    Task DeleteAsync(TodoItem item);
}
  • Der Port: Core definiert die benötigte Schnittstelle.
  • Keine Abhängigkeit zu sqlite-net-pcl oder EF Core.

Infrastructure: TodoItemEntity + Mapping

internal class TodoItemEntity
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public bool Done { get; set; }

    public TodoItem ToModel() =>
        new() { Id = Id, Title = Title, Done = Done };
}
  • [PrimaryKey, AutoIncrement] bleibt in der Infrastructure.
  • Explizites Mapping zeigt die Grenze zwischen den Schichten.

Core: TodoViewModel

public partial class TodoViewModel : ObservableObject
{
    private readonly ITodoRepository _repository;
    public ObservableCollection<TodoItem> Todos { get; } = [];

    [ObservableProperty] private string _newTitle = string.Empty;

    [RelayCommand] public async Task LoadAsync() { ... }
    [RelayCommand] public async Task AddAsync() { ... }
    [RelayCommand] public async Task ToggleDoneAsync(TodoItem item) { ... }
    [RelayCommand] public async Task DeleteAsync(TodoItem item) { ... }
}
  • [RelayCommand] generiert LoadCommand, AddCommand etc. automatisch.
  • ViewModel kennt nur ITodoRepository — ohne MAUI und ohne SQLite testbar.

App: Dependency Injection

var dbPath = Path.Combine(FileSystem.AppDataDirectory, "todos.db3");
builder.Services.AddSingleton<ITodoRepository>(
    _ => new SqliteTodoRepository(dbPath));
builder.Services.AddSingleton<TodoViewModel>();
builder.Services.AddSingleton<MainPage>();
  • FileSystem.AppDataDirectory ist nur im MAUI-Projekt verfügbar.
  • Interface → Implementierung per Factory-Lambda.

Tests: FakeTodoRepository

internal class FakeTodoRepository : ITodoRepository
{
    private readonly List<TodoItem> _items = [];
    private int _nextId = 1;

    public Task<List<TodoItem>> GetAllAsync() =>
        Task.FromResult(_items.ToList());

    public Task SaveAsync(TodoItem item)
    {
        if (item.Id == 0) { item.Id = _nextId++; _items.Add(item); }
        return Task.CompletedTask;
    }

    public Task DeleteAsync(TodoItem item)
    {
        _items.RemoveAll(i => i.Id == item.Id);
        return Task.CompletedTask;
    }
}
  • Kein SQLite, kein MAUI — reines C# in-memory.

Tests: Beispiel

[Fact]
public async Task AddAsync_AddsItemToTodos()
{
    var repo = new FakeTodoRepository();
    var vm = new TodoViewModel(repo);
    vm.NewTitle = "Test";

    await vm.AddAsync();

    Assert.Single(vm.Todos);
    Assert.Equal("Test", vm.Todos[0].Title);
}
  • 5 Tests: Add, EmptyTitleNoAdd, Delete, ToggleTrue, ToggleFalseBack.

Vergleich

Agent-Beispiel vs. manuelles Beispiel

Kriterium Manuell (SQLiteRecipe) Agent (csharp-maui-sqlite)
Architektur 4 Projekte (veraltet) Core / Infrastructure / App / Tests
Muster proprietär, veraltet MVVM, Ports & Adapters, DI
Unit Tests keine 5 xUnit-Tests
Didaktische Klarheit niedrig (veraltet) hoch (modern, schrittweise erklärbar)
Zielgruppe fortgeschrittene Studierende

Empfehlung

  • Agent-Version ersetzt das manuelle Beispiel vollständig.
  • Zeigt MVVM, Ports & Adapters und Unit Tests in einem überschaubaren Beispiel.
  • Einstieg für Clean Architecture / Hexagonal Architecture auf Mobile.

Entity Framework Core

Beispielprojekt

Vorgehen

Agent-generated example: csharp-maui-efcore

  • Ziel: dasselbe Todo-Beispiel — aber mit EF Core statt sqlite-net-pcl
  • Mehrwert: Wer EF Core aus ASP.NET Core kennt, sieht wie es sich auf Mobile überträgt
  • ursprünglich out of scope: MVVM, DI — nach Refaktorierung jetzt enthalten
  • weiterhin out of scope: Migrations, mehrere Tabellen

Verwendete Instructions

  • System- und Agent-Regeln: kleinster sinnvoller Scope, erst Kontext lesen, Änderungen verifizieren
  • .github/copilot-instructions.md: kleine, reviewbare Änderungen und echte Verifikation
  • .github/instructions/artifacts-consumers-education.instructions.md: didaktische Klarheit, ein Hauptkonzept
  • .github/instructions/slides.instructions.md: knappe, gut scannbare Folien

Exakter Prompt

ja, gerne als beispiel. dann mit deinem repo?
[Repo: https://github.com/RobinNunkesser/csharp-maui-efcore.git ist angelegt]

Ergebnis

Projektstruktur

csharp-maui-efcore/
├── EFCore.Core/           # Domain + Interface + ViewModel
├── EFCore.Infrastructure/ # EF Core Adapter
├── EFCore/                # MAUI App (Composition Root)
└── EFCore.Tests/          # xUnit (nur Core)
  • Identische Struktur wie csharp-maui-sqlite.
  • Austausch der Infrastructure — Core und Tests bleiben unverändert.

Infrastructure: TodoDbContext (intern)

internal class TodoDbContext : DbContext
{
    private readonly string _dbPath;

    public DbSet<TodoItem> Todos { get; set; } = null!;

    public TodoDbContext(string dbPath) { _dbPath = dbPath; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite($"Data Source={_dbPath}");
}
  • internal — nur innerhalb der Infrastructure sichtbar.
  • Kein [PrimaryKey]-Attribut nötig: EF Core erkennt Id per Convention.

Infrastructure: EfCoreTodoRepository

public class EfCoreTodoRepository : ITodoRepository
{
    private readonly string _dbPath;

    public EfCoreTodoRepository(string dbPath)
    {
        _dbPath = dbPath;
        using var ctx = CreateContext();
        ctx.Database.EnsureCreated();
    }

    private TodoDbContext CreateContext() => new(_dbPath);

    public async Task<List<TodoItem>> GetAllAsync()
    {
        await using var ctx = CreateContext();
        return await ctx.Todos.ToListAsync();
    }

    // SaveAsync, DeleteAsync analog
}
  • Per-Operation-Context: kurze Lebensdauer, kein Tracking-Problem.
  • EnsureCreated() im Konstruktor statt im OnAppearing.

Schlüsselunterschied: kein Entitäts-Mapping

Aspekt sqlite-net-pcl EF Core
Entitäts-Mapping TodoItemEntity + manuell TodoItem direkt (Convention)
Attribute [PrimaryKey, AutoIncrement] keine
Kontext SQLiteAsyncConnection DbContext (intern)
LINQ-Unterstützung eingeschränkt vollständig

Warum dieselbe Architektur?

  • Core (ITodoRepository, TodoItem, TodoViewModel) ist identisch.
  • Nur die Infrastructure ändert sich — das zeigt Ports & Adapters in der Praxis.
  • Tests bleiben unverändert: FakeTodoRepository funktioniert für beide Beispiele.

EF Core vs. sqlite-net-pcl

Kriterium sqlite-net-pcl EF Core
Paketgröße klein größer
API eigene Klassen vertrautes EF-Core-Muster
Migrations nicht vorhanden möglich
Zielgruppe SQLite-Einführung Wer EF Core schon kennt

Empfehlung

  • Für eine Einführung in Persistenz: sqlite-net-pcl (direkter, weniger Abstraktion).
  • Für Kurse, in denen EF Core Thema ist: EF Core zeigt die Portierbarkeit auf Mobile.
  • Beide Beispiele teilen Core und Tests — das ist der eigentliche didaktische Mehrwert.