Zum Inhalt springen

Von .NET 3.5 zu modernem ASP.NET Core: Ein strukturierter Lernpfad

Veröffentlicht am 27. Feb. 2026 | ca. 9 Min. Lesezeit |

Phase 0: Orientierung — Was hat sich seit .NET 3.5 geändert?

Bevor es mit den praktischen Übungen losgeht, lohnt sich ein Überblick über die wichtigsten Veränderungen. Nicht alles davon muss sofort verstanden werden — dieser Abschnitt dient als Nachschlagewerk.

Die .NET-Landschaft heute

Typischer Ausgangspunkt (.NET 3.5)  Heute (2025/2026)
──────────────────────              ─────────────────
.NET Framework 3.5                  .NET 9 / .NET 10 (Preview)
Windows-only                        Cross-platform (Win/Linux/macOS)
Visual Studio Pflicht               VS Code + CLI reicht völlig
MSBuild + .sln komplex              dotnet CLI für alles
NuGet war ganz neu                  NuGet ist das zentrale Ökosystem
Web: ASP.NET WebForms               Web: ASP.NET Core (komplett neu)
ORM: LINQ to SQL / EF 1.0          ORM: Entity Framework Core 9
Kein DI eingebaut                   DI ist Kernkonzept
Kein async/await                    async/await ist überall
GUI: WinForms / WPF                 GUI: MAUI / Blazor / Avalonia

Die wichtigsten neuen C#-Features (nach Version)

Version Highlight Warum es wichtig ist
C# 5 (2012) async / await Asynchrone Programmierung ohne Callback-Hölle. Das wichtigste Feature für ASP.NET Core.
C# 6 (2015) String Interpolation $"Hallo {name}", Null-Conditional ?. Weniger Boilerplate, sichererer Null-Zugriff
C# 7 (2017) Pattern Matching, Tuples (int x, string y), out var Expressiverer Code, weniger temporäre Variablen
C# 8 (2019) Nullable Reference Types string?, Switch Expressions, Default Interface Methods Null-Sicherheit zur Compile-Zeit, funktionalerer Stil
C# 9 (2020) record Typen, Top-Level Statements, init-only Properties Immutable Datentypen, weniger Boilerplate
C# 10 (2021) Global Usings, File-Scoped Namespaces Deutlich weniger Code pro Datei
C# 11 (2022) Raw String Literals """...""", Required Members Multiline-Strings ohne Escaping
C# 12 (2023) Primary Constructors, Collection Expressions [1, 2, 3] Noch kompakterer Code
C# 13 (2024) params Collections, Lock Typ Modernisierte Grundbausteine

Schneller Vergleich: Alter vs. neuer Stil

// ══════════════════════════════════════════════════
// KLASSISCHER STIL (.NET 3.5, C# 3.0)
// ══════════════════════════════════════════════════

using System;
using System.Collections.Generic;

namespace MeineApp
{
    public class Benutzer
    {
        private string _name;
        private string _email;

        public Benutzer(string name, string email)
        {
            _name = name;
            _email = email;
        }

        public string Name { get { return _name; } set { _name = value; } }
        public string Email { get { return _email; } set { _email = value; } }

        public string GetDisplayName()
        {
            if (_name != null)
                return _name;
            else
                return "Unbekannt";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var benutzer = new Benutzer("Max", "[email protected]");
            Console.WriteLine(string.Format("Hallo {0}!", benutzer.Name));
        }
    }
}

// ══════════════════════════════════════════════════
// MODERNER STIL (.NET 9, C# 13)
// ══════════════════════════════════════════════════

// File-Scoped Namespace (C# 10) — kein Extra-Indent mehr
namespace MeineApp;

// Record statt Klasse für Daten (C# 9) — immutable, mit Equals/GetHashCode
public record Benutzer(string Name, string Email)
{
    // Expression-bodied Member + Null-Conditional (C# 6) + Pattern Matching (C# 8)
    public string DisplayName => Name ?? "Unbekannt";
}

// Top-Level Statement (C# 9) — kein Main() mehr nötig (für Konsolen-Apps)
var benutzer = new Benutzer("Max", "[email protected]");
Console.WriteLine($"Hallo {benutzer.Name}!");  // String Interpolation (C# 6)

Phase 1: Modernes C# auffrischen (1–2 Wochen)

Ziel: Die Sprache wiedererkennen und die neuen Features verstehen.

C# muss nicht von Grund auf neu gelernt werden — die Grundlagen sind bekannt. Ein paar Features sind jedoch fundamental neu und werden täglich in ASP.NET Core benötigt.

1.1 Übungsprojekt anlegen

mkdir ~/lernpfad && cd ~/lernpfad
dotnet new console -n CSharpAuffrischung
cd CSharpAuffrischung
code .    # VS Code öffnen

1.2 Die drei wichtigsten Features zum Üben

Feature 1: async/await (PFLICHT — ohne das geht nichts in ASP.NET Core)

// Jeder API-Request in ASP.NET Core ist async.
// Jede Datenbankabfrage ist async. Jeder HTTP-Call ist async.

// Synchron (klassischer Stil):
public string HoleDaten()
{
    Thread.Sleep(2000);  // Blockiert den Thread 2 Sekunden
    return "Daten";
}

// Asynchron (moderner Stil):
public async Task<string> HoleDatenAsync()
{
    await Task.Delay(2000);  // Thread ist frei für andere Arbeit!
    return "Daten";
}

// Aufruf:
var daten = await HoleDatenAsync();

// GOLDENE REGEL: Wenn await benutzt wird, muss die Methode async sein.
// Wenn die Methode async ist, gibt sie Task<T> statt T zurück.
// Und: async void ist VERBOTEN (außer bei Event-Handlern).

Übung 1: Schreibe ein Konsolenprogramm, das gleichzeitig 3 „API-Calls" simuliert (je 1–3 Sekunden Task.Delay) und die Ergebnisse sammelt. Nutze Task.WhenAll().

Feature 2: Nullable Reference Types

// In der .csproj steht standardmäßig bei neuen Projekten:
// <Nullable>enable</Nullable>

string name = "Max";       // Darf NICHT null sein
string? nickname = null;   // Darf null sein (das ? erlaubt es)

// Compiler warnt:
Console.WriteLine(nickname.Length);   // ⚠️ Warning: mögliche NullReferenceException

// Korrekte Verwendung:
if (nickname is not null)
{
    Console.WriteLine(nickname.Length);  // ✅ Sicher
}

// Oder kürzer mit Pattern Matching:
Console.WriteLine(nickname?.Length ?? 0);  // ✅ Null-safe

Übung 2: Erstelle eine Klasse UserProfile mit nullable und non-nullable Properties und schreibe eine Methode, die null-safe darauf zugreift.

Feature 3: Records und Pattern Matching

// Record: Immutable Datentyp mit automatischem Equals, GetHashCode, ToString
public record Note(int Id, string Title, string Content, DateTime CreatedAt);

// Erstellen:
var note = new Note(1, "Einkaufsliste", "Milch, Brot", DateTime.UtcNow);

// Kopie mit Änderung (with-Expression):
var updated = note with { Title = "Neue Einkaufsliste" };

// Pattern Matching in switch:
string Beschreibung(Note n) => n switch
{
    { Title.Length: > 50 } => "Langer Titel",
    { Content.Length: 0 }  => "Leere Notiz",
    _                      => "Normale Notiz"
};

Übung 3: Modelliere die Kernentitäten eines typischen Webprojekts als Records: UserProfile, Post, FriendRequest. Schreibe Switch Expressions, die unterschiedliche Aktionen je nach Status ausführen.

1.3 Empfohlene Lernressourcen

  • Microsoft Learn: "What's new in C#" — eine Seite pro Version, ideal zum Überfliegen https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/
  • Microsoft Learn: "Asynchronous programming" — das Deep-Dive-Tutorial https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/
  • "C# in a Nutshell" von Joseph Albahari — das Referenzbuch für C# (deckt alle Versionen ab)

Phase 2: ASP.NET Core Grundlagen (2–3 Wochen)

Ziel: Einen REST-API-Server bauen können.

2.1 Erstes Web-API-Projekt

cd ~/lernpfad
dotnet new webapi -n MeineApi
cd MeineApi
dotnet run
# Öffne http://localhost:5000/weatherforecast im Browser

2.2 Die Kernkonzepte (in dieser Reihenfolge lernen)

Konzept 1: Dependency Injection (DI)

In .NET 3.5 wurden Objekte mit new erstellt. In ASP.NET Core werden Services im DI-Container registriert und automatisch injiziert:

// Service registrieren (in Program.cs):
builder.Services.AddScoped<INoteService, NoteService>();

// Service nutzen (im Controller — wird automatisch injiziert):
public class NotesController : ControllerBase
{
    private readonly INoteService _noteService;

    public NotesController(INoteService noteService)  // DI!
    {
        _noteService = noteService;
    }
}

Drei Lifetimes verstehen: AddTransient (jedes Mal neu), AddScoped (einmal pro Request), AddSingleton (einmal für die App).

Konzept 2: Middleware-Pipeline

Jeder HTTP-Request durchläuft eine Kette von Middleware-Komponenten:

Request → Logging → Auth → Routing → Controller → Response
                                         ↓
                                    Datenbank
// Program.cs — die Reihenfolge ist wichtig!
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();    // Erst Auth prüfen
app.UseAuthorization();     // Dann Berechtigung prüfen
app.MapControllers();       // Dann Request verarbeiten
app.Run();

Konzept 3: Controller + Routing

[ApiController]
[Route("api/[controller]")]   // → /api/notes
public class NotesController : ControllerBase
{
    [HttpGet]                          // GET /api/notes
    public async Task<ActionResult<List<Note>>> GetAll() { ... }

    [HttpGet("{id}")]                  // GET /api/notes/42
    public async Task<ActionResult<Note>> GetById(int id) { ... }

    [HttpPost]                         // POST /api/notes
    public async Task<ActionResult<Note>> Create(CreateNoteDto dto) { ... }

    [HttpPut("{id}")]                  // PUT /api/notes/42
    public async Task<ActionResult> Update(int id, UpdateNoteDto dto) { ... }

    [HttpDelete("{id}")]               // DELETE /api/notes/42
    public async Task<ActionResult> Delete(int id) { ... }
}

Konzept 4: Entity Framework Core + PostgreSQL

# Pakete installieren
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
// DbContext — Tor zur Datenbank
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Note> Notes => Set<Note>();
    public DbSet<User> Users => Set<User>();
}

// Entity
public class Note
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public string? Content { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

// In Program.cs registrieren:
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

// Migration erstellen + ausführen:
// dotnet ef migrations add InitialCreate
// dotnet ef database update

2.3 Übungsprojekt: Notizen-API

Checkliste für eine vollständige CRUD-API:

  • [ ] dotnet new webapi Projekt erstellt
  • [ ] Note-Entity + AppDbContext definiert
  • [ ] PostgreSQL-Connection in appsettings.json
  • [ ] Migration erstellt und ausgeführt
  • [ ] NotesController mit allen 5 CRUD-Endpunkten
  • [ ] Alle Endpunkte sind async
  • [ ] DTOs für Create und Update (nicht die Entity direkt exponieren!)
  • [ ] Swagger UI funktioniert unter /swagger
  • [ ] Mit curl oder Swagger alle Endpunkte getestet

2.4 Empfohlene Lernressourcen

  • Microsoft Learn: "Tutorial: Create a web API with ASP.NET Core" https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-web-api
  • Microsoft Learn: "Entity Framework Core — Getting Started" https://learn.microsoft.com/en-us/ef/core/get-started/overview/first-app

Phase 3: Authentifizierung & Sicherheit (1–2 Wochen)

Ziel: JWT-basierte Authentifizierung implementieren — unverzichtbar für abgesicherte APIs.

3.1 JWT-Authentifizierung einrichten

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

// Controller schützen:
[Authorize]   // Nur mit gültigem JWT-Token erreichbar
[HttpGet("profile")]
public async Task<ActionResult<UserProfile>> GetProfile()
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    // ...
}

3.2 Übungsprojekt erweitern

  • [ ] User-Entity mit Passwort-Hash (BCrypt oder ASP.NET Core Identity)
  • [ ] Login-Endpunkt, der JWT-Token zurückgibt
  • [ ] Register-Endpunkt
  • [ ] Notizen-Endpunkte mit [Authorize] geschützt
  • [ ] Jeder User sieht nur seine eigenen Notizen
  • [ ] Refresh-Token-Mechanismus (optional, aber empfohlen)

Phase 4: Echtzeit-Kommunikation mit SignalR (1 Woche)

Ziel: WebSocket-basierte Echtzeit-Features verstehen — für Chat, Notifications, Live-Feed.

4.1 SignalR einrichten

// Hub definieren:
public class NotificationHub : Hub
{
    public async Task SendNotification(string userId, string message)
    {
        await Clients.User(userId).SendAsync("ReceiveNotification", message);
    }
}

// In Program.cs:
builder.Services.AddSignalR();
app.MapHub<NotificationHub>("/hubs/notifications");

4.2 Übung

  • [ ] NotificationHub erstellt
  • [ ] Einfachen HTML/JS-Client, der Notifications empfängt
  • [ ] Wenn ein Datensatz erstellt wird, bekommen alle verbundenen Clients eine Echtzeit-Nachricht

Phase 5: Message Queue mit RabbitMQ (1 Woche)

Ziel: Asynchrone Hintergrund-Jobs verstehen — für Bildkomprimierung, E-Mail-Versand, Feed-Updates.

5.1 MassTransit + RabbitMQ

dotnet add package MassTransit.RabbitMQ
// Message definieren:
public record ImageUploadedEvent(Guid ImageId, string UserId, string FilePath);

// Consumer (Worker):
public class ImageCompressionConsumer : IConsumer<ImageUploadedEvent>
{
    public async Task Consume(ConsumeContext<ImageUploadedEvent> context)
    {
        var msg = context.Message;
        // Bild komprimieren...
        Console.WriteLine($"Komprimiere Bild {msg.ImageId} für User {msg.UserId}");
    }
}

// In Program.cs registrieren:
builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<ImageCompressionConsumer>();
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("localhost", "/", h =>
        {
            h.Username("guest");
            h.Password("guest");
        });
        cfg.ConfigureEndpoints(context);
    });
});

5.2 Übung

  • [ ] RabbitMQ im Docker-Container gestartet
  • [ ] Wenn ein Bild hochgeladen wird → Event in die Queue
  • [ ] Consumer verarbeitet das Event asynchron
  • [ ] Ergebnis: Upload-Endpunkt antwortet sofort, Verarbeitung läuft im Hintergrund

Phase 6: Neo4j-Integration (1 Woche)

Ziel: Die Graph-Datenbank anbinden — für Beziehungsdaten, Empfehlungen, Graphabfragen.

6.1 Neo4j .NET Driver

dotnet add package Neo4j.Driver
// Service für Neo4j:
public class GraphService
{
    private readonly IDriver _driver;

    public GraphService(IConfiguration config)
    {
        _driver = GraphDatabase.Driver(
            config["Neo4j:Uri"],
            AuthTokens.Basic(config["Neo4j:User"], config["Neo4j:Password"]));
    }

    public async Task<List<string>> GetRelatedUsers(string userId)
    {
        await using var session = _driver.AsyncSession();
        var result = await session.RunAsync(
            @"MATCH (me:User {id: $userId})-[:FRIEND]->()-[:FRIEND]->(fof:User)
              WHERE NOT (me)-[:FRIEND]->(fof) AND fof.id <> $userId
              RETURN DISTINCT fof.name AS name
              LIMIT 10",
            new { userId });

        return await result.ToListAsync(r => r["name"].As<string>());
    }
}

// In Program.cs:
builder.Services.AddSingleton<GraphService>();

6.2 Übung

  • [ ] Neo4j im Docker-Container gestartet
  • [ ] Notiz-Nodes und Tag-Relationships erstellt
  • [ ] "Verwandte Notizen"-Abfragen funktionieren (z.B. MATCH (n:Note)-[:TAGGED]->(t:Tag)<-[:TAGGED]-(related:Note) RETURN related)
  • [ ] "Gemeinsame Tags"-Abfrage implementiert

Phase 7: Docker + Zusammenführung (1 Woche)

Ziel: Den gesamten Stack in Docker Compose zusammenführen.

7.1 docker-compose.yml für den gesamten Stack

services:
  api:
    build: .
    ports: ["5000:8080"]
    depends_on: [postgres, neo4j, rabbitmq, redis]
    environment:
      - ConnectionStrings__Default=Host=postgres;Database=appdb;Username=app;Password=secret
      - Neo4j__Uri=bolt://neo4j:7687

  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes: [pgdata:/var/lib/postgresql/data]

  neo4j:
    image: neo4j:5
    environment:
      NEO4J_AUTH: neo4j/secret123
    ports: ["7474:7474", "7687:7687"]
    volumes: [neo4jdata:/data]

  rabbitmq:
    image: rabbitmq:3-management
    ports: ["5672:5672", "15672:15672"]

  redis:
    image: redis:7-alpine
    ports: ["6379:6379"]

volumes:
  pgdata:
  neo4jdata:

7.2 Abschluss-Checkliste

  • [ ] docker compose up startet den gesamten Stack
  • [ ] API antwortet auf http://localhost:5000/swagger
  • [ ] Benutzerregistrierung + Login → JWT-Token
  • [ ] CRUD für Hauptentitäten funktioniert (PostgreSQL)
  • [ ] Beziehungen anlegen + abfragen (Neo4j)
  • [ ] Datei-Upload löst asynchrone Verarbeitung aus (RabbitMQ)
  • [ ] Echtzeit-Benachrichtigungen funktionieren (SignalR)
  • [ ] Redis wird für Session/Cache genutzt

Zeitplan-Übersicht

Phase Thema Dauer Priorität
0 Orientierung: Was hat sich geändert? 1–2 Tage Einmalig lesen
1 Modernes C# (async/await, Records, Nullable) 1–2 Wochen ⭐⭐⭐⭐⭐
2 ASP.NET Core Grundlagen (API, EF Core, DI) 2–3 Wochen ⭐⭐⭐⭐⭐
3 JWT-Authentifizierung 1–2 Wochen ⭐⭐⭐⭐⭐
4 SignalR (Echtzeit) 1 Woche ⭐⭐⭐⭐
5 RabbitMQ + MassTransit 1 Woche ⭐⭐⭐⭐
6 Neo4j-Integration 1 Woche ⭐⭐⭐⭐
7 Docker + Zusammenführung 1 Woche ⭐⭐⭐

Geschätzte Gesamtdauer: 8–12 Wochen bei abendlichem/Wochenend-Lernen.


VS Code Extensions

Empfohlene Extensions für die .NET-Entwicklung:

code --install-extension ms-dotnettools.csdevkit          # C# Dev Kit
code --install-extension ms-dotnettools.csharp            # C# Sprachunterstützung
code --install-extension ms-dotnettools.dotnet-interactive-vscode  # .NET Interactive
code --install-extension ms-azuretools.vscode-docker      # Docker
code --install-extension humao.rest-client                # REST-Client (statt Postman)
Thomas Wunner

Thomas Wunner

Fachinformatiker für Anwendungsentwicklung mit Ausbildereignungsprüfung und über 14 Jahre Erfahrung im Aufbau skalierbarer Webanwendungen mit Symfony und Shopware. Abseits der Tastatur ist Thomas als Rettungsschwimmer in der Wasserwacht aktiv, legt als DJ auf und erkundet die Umgebung auf dem Motorrad.

Kommentare

Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.