Skip to content

From .NET 3.5 to Modern ASP.NET Core: A Structured Learning Path

Published on Feb 27, 2026 | approx. 9 min read |

Phase 0: Orientation — What Has Changed Since .NET 3.5?

Before diving into practical exercises, it is worth getting an overview of the most important changes. Not all of this needs to be understood immediately — this section serves as a reference.

The .NET Landscape Today

Typical Starting Point (.NET 3.5)  Today (2025/2026)
──────────────────────              ─────────────────
.NET Framework 3.5                  .NET 9 / .NET 10 (Preview)
Windows-only                        Cross-platform (Win/Linux/macOS)
Visual Studio required              VS Code + CLI is perfectly sufficient
MSBuild + .sln complex              dotnet CLI for everything
NuGet was brand new                 NuGet is the central ecosystem
Web: ASP.NET WebForms               Web: ASP.NET Core (completely rewritten)
ORM: LINQ to SQL / EF 1.0          ORM: Entity Framework Core 9
No built-in DI                      DI is a core concept
No async/await                      async/await is everywhere
GUI: WinForms / WPF                 GUI: MAUI / Blazor / Avalonia

The Most Important New C# Features (by Version)

Version Highlight Why It Matters
C# 5 (2012) async / await Asynchronous programming without callback hell. The most important feature for ASP.NET Core.
C# 6 (2015) String Interpolation $"Hello {name}", Null-Conditional ?. Less boilerplate, safer null access
C# 7 (2017) Pattern Matching, Tuples (int x, string y), out var More expressive code, fewer temporary variables
C# 8 (2019) Nullable Reference Types string?, Switch Expressions, Default Interface Methods Compile-time null safety, more functional style
C# 9 (2020) record Types, Top-Level Statements, init-only Properties Immutable data types, less boilerplate
C# 10 (2021) Global Usings, File-Scoped Namespaces Significantly less code per file
C# 11 (2022) Raw String Literals """...""", Required Members Multiline strings without escaping
C# 12 (2023) Primary Constructors, Collection Expressions [1, 2, 3] Even more compact code
C# 13 (2024) params Collections, Lock Type Modernised building blocks

Quick Comparison: Old vs. New Style

// ══════════════════════════════════════════════════
// CLASSIC STYLE (.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));
        }
    }
}

// ══════════════════════════════════════════════════
// MODERN STYLE (.NET 9, C# 13)
// ══════════════════════════════════════════════════

// File-Scoped Namespace (C# 10) — no extra indentation needed
namespace MeineApp;

// Record instead of class for data (C# 9) — immutable, with 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) — no Main() needed anymore (for console apps)
var benutzer = new Benutzer("Max", "[email protected]");
Console.WriteLine($"Hallo {benutzer.Name}!");  // String Interpolation (C# 6)

Phase 1: Refreshing Modern C# (1–2 Weeks)

Goal: Recognise the language again and understand the new features.

C# does not need to be learned from scratch — the fundamentals are already known. However, a few features are fundamentally new and are used daily in ASP.NET Core.

1.1 Creating a Practice Project

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

1.2 The Three Most Important Features to Practise

Feature 1: async/await (MANDATORY — nothing works without it in ASP.NET Core)

// Every API request in ASP.NET Core is async.
// Every database query is async. Every HTTP call is async.

// Synchronous (classic style):
public string HoleDaten()
{
    Thread.Sleep(2000);  // Blocks the thread for 2 seconds
    return "Daten";
}

// Asynchronous (modern style):
public async Task<string> HoleDatenAsync()
{
    await Task.Delay(2000);  // Thread is free for other work!
    return "Daten";
}

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

// GOLDEN RULE: If await is used, the method must be async.
// If the method is async, it returns Task<T> instead of T.
// And: async void is FORBIDDEN (except for event handlers).

Exercise 1: Write a console program that simultaneously simulates 3 "API calls" (each with 1–3 seconds of Task.Delay) and collects the results. Use Task.WhenAll().

Feature 2: Nullable Reference Types

// New projects have this in the .csproj by default:
// <Nullable>enable</Nullable>

string name = "Max";       // CANNOT be null
string? nickname = null;   // CAN be null (the ? allows it)

// Compiler warns:
Console.WriteLine(nickname.Length);   // ⚠️ Warning: possible NullReferenceException

// Correct usage:
if (nickname is not null)
{
    Console.WriteLine(nickname.Length);  // ✅ Safe
}

// Or shorter with Pattern Matching:
Console.WriteLine(nickname?.Length ?? 0);  // ✅ Null-safe

Exercise 2: Create a UserProfile class with nullable and non-nullable properties and write a method that accesses them in a null-safe manner.

Feature 3: Records and Pattern Matching

// Record: Immutable data type with automatic Equals, GetHashCode, ToString
public record Note(int Id, string Title, string Content, DateTime CreatedAt);

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

// Copy with modification (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"
};

Exercise 3: Model the core entities of a typical web project as records: UserProfile, Post, FriendRequest. Write switch expressions that perform different actions depending on the status.

1.3 Recommended Learning Resources

  • Microsoft Learn: "What's new in C#" — one page per version, ideal for skimming https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/
  • Microsoft Learn: "Asynchronous programming" — the deep-dive tutorial https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/
  • "C# in a Nutshell" by Joseph Albahari — the reference book for C# (covers all versions)

Phase 2: ASP.NET Core Fundamentals (2–3 Weeks)

Goal: Be able to build a REST API server.

2.1 First Web API Project

cd ~/lernpfad
dotnet new webapi -n MeineApi
cd MeineApi
dotnet run
# Open http://localhost:5000/weatherforecast in the browser

2.2 The Core Concepts (Learn in This Order)

Concept 1: Dependency Injection (DI)

In .NET 3.5, objects were created with new. In ASP.NET Core, services are registered in the DI container and automatically injected:

// Register a service (in Program.cs):
builder.Services.AddScoped<INoteService, NoteService>();

// Use a service (in the controller — automatically injected):
public class NotesController : ControllerBase
{
    private readonly INoteService _noteService;

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

Understand three lifetimes: AddTransient (new every time), AddScoped (once per request), AddSingleton (once for the entire app).

Concept 2: Middleware Pipeline

Every HTTP request passes through a chain of middleware components:

Request → Logging → Auth → Routing → Controller → Response
                                         ↓
                                    Database
// Program.cs — the order matters!
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();    // First check authentication
app.UseAuthorization();     // Then check authorisation
app.MapControllers();       // Then process the request
app.Run();

Concept 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) { ... }
}

Concept 4: Entity Framework Core + PostgreSQL

# Install packages
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
// DbContext — gateway to the database
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;
}

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

// Create and run migration:
// dotnet ef migrations add InitialCreate
// dotnet ef database update

2.3 Practice Project: Notes API

Checklist for a complete CRUD API:

  • [ ] dotnet new webapi project created
  • [ ] Note entity + AppDbContext defined
  • [ ] PostgreSQL connection in appsettings.json
  • [ ] Migration created and executed
  • [ ] NotesController with all 5 CRUD endpoints
  • [ ] All endpoints are async
  • [ ] DTOs for Create and Update (do not expose the entity directly!)
  • [ ] Swagger UI works at /swagger
  • [ ] All endpoints tested with curl or Swagger

2.4 Recommended Learning Resources

  • 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: Authentication & Security (1–2 Weeks)

Goal: Implement JWT-based authentication — essential for secured APIs.

3.1 Setting Up JWT Authentication

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"]!))
        };
    });

// Protect a controller:
[Authorize]   // Only accessible with a valid JWT token
[HttpGet("profile")]
public async Task<ActionResult<UserProfile>> GetProfile()
{
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    // ...
}

3.2 Extending the Practice Project

  • [ ] User entity with password hash (BCrypt or ASP.NET Core Identity)
  • [ ] Login endpoint that returns a JWT token
  • [ ] Register endpoint
  • [ ] Notes endpoints protected with [Authorize]
  • [ ] Each user only sees their own notes
  • [ ] Refresh token mechanism (optional but recommended)

Phase 4: Real-Time Communication with SignalR (1 Week)

Goal: Understand WebSocket-based real-time features — for chat, notifications, live feeds.

4.1 Setting Up SignalR

// Define a Hub:
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 Exercise

  • [ ] NotificationHub created
  • [ ] Simple HTML/JS client that receives notifications
  • [ ] When a record is created, all connected clients receive a real-time message

Phase 5: Message Queue with RabbitMQ (1 Week)

Goal: Understand asynchronous background jobs — for image compression, email delivery, feed updates.

5.1 MassTransit + RabbitMQ

dotnet add package MassTransit.RabbitMQ
// Define a message:
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;
        // Compress image...
        Console.WriteLine($"Komprimiere Bild {msg.ImageId} für User {msg.UserId}");
    }
}

// Register in Program.cs:
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 Exercise

  • [ ] RabbitMQ running in a Docker container
  • [ ] When an image is uploaded, an event is sent to the queue
  • [ ] Consumer processes the event asynchronously
  • [ ] Result: The upload endpoint responds immediately while processing runs in the background

Phase 6: Neo4j Integration (1 Week)

Goal: Connect the graph database — for relationship data, recommendations, graph queries.

6.1 Neo4j .NET Driver

dotnet add package Neo4j.Driver
// Service for 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 Exercise

  • [ ] Neo4j running in a Docker container
  • [ ] Note nodes and tag relationships created
  • [ ] "Related notes" queries work (e.g. MATCH (n:Note)-[:TAGGED]->(t:Tag)<-[:TAGGED]-(related:Note) RETURN related)
  • [ ] "Shared tags" query implemented

Phase 7: Docker + Bringing It All Together (1 Week)

Goal: Bring the entire stack together in Docker Compose.

7.1 docker-compose.yml for the Full 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 Final Checklist

  • [ ] docker compose up starts the entire stack
  • [ ] API responds at http://localhost:5000/swagger
  • [ ] User registration + login returns a JWT token
  • [ ] CRUD for main entities works (PostgreSQL)
  • [ ] Relationships can be created and queried (Neo4j)
  • [ ] File upload triggers asynchronous processing (RabbitMQ)
  • [ ] Real-time notifications work (SignalR)
  • [ ] Redis is used for session/cache

Timeline Overview

Phase Topic Duration Priority
0 Orientation: What has changed? 1–2 days Read once
1 Modern C# (async/await, Records, Nullable) 1–2 weeks ⭐⭐⭐⭐⭐
2 ASP.NET Core Fundamentals (API, EF Core, DI) 2–3 weeks ⭐⭐⭐⭐⭐
3 JWT Authentication 1–2 weeks ⭐⭐⭐⭐⭐
4 SignalR (Real-Time) 1 week ⭐⭐⭐⭐
5 RabbitMQ + MassTransit 1 week ⭐⭐⭐⭐
6 Neo4j Integration 1 week ⭐⭐⭐⭐
7 Docker + Bringing It All Together 1 week ⭐⭐⭐

Estimated total duration: 8–12 weeks studying evenings and weekends.


VS Code Extensions

Recommended extensions for .NET development:

code --install-extension ms-dotnettools.csdevkit          # C# Dev Kit
code --install-extension ms-dotnettools.csharp            # C# Language Support
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 (instead of Postman)
Thomas Wunner

Thomas Wunner

Certified IT specialist for application development with an instructor qualification and over 14 years of experience building scalable web applications with Symfony and Shopware. When not coding, Thomas volunteers as a lifeguard with the Wasserwacht, performs as a DJ, and explores the countryside on his motorbike.

Comments

Comments are provided by Remark42. By loading comments, data is transmitted to our comment server.