Complete Guide · 12 Sections

Complete C# for Dummies

The full stack in one page — from Hello, World! through concurrent collections, ASP.NET Core, Identity, SQL Server and GraphQL. Plain English, copy-paste .NET 9 code, no gaps.

For Dummies C# & .NET 9 ASP.NET Core GraphQL SQL Server

New here? Start with C# for Dummies if you've never coded. This page is the complete reference — language, concurrency, web, auth, database and GraphQL in one place. Tutorial content is in English; use the ΕΛ button on the home page for Greek UI labels.

01

C# & .NET — The Big Picture

C# is the language. .NET is the runtime + libraries that execute it. You write .cs files, the compiler turns them into IL (intermediate language), and the .NET runtime (CLR) executes them on Windows, macOS or Linux.

One language, many app types: console apps, web APIs, Blazor sites, desktop (WPF/WinForms), games (Unity), mobile (.NET MAUI), cloud (Azure Functions, Table Storage & Service Bus).

1.1

Your First Program

Create a console app, run it, understand the entry point.

// Terminal:
// dotnet new console -n HelloApp
// cd HelloApp
// dotnet run

// Program.cs — .NET 9 top-level statements
Console.WriteLine("Hello, World!");

var name = "Spyros";
Console.WriteLine($"Welcome, {name}!");
1.2

Project Structure

What every .NET project contains and what each file does.

MyApp/
├── MyApp.csproj      ← project file (target framework, packages)
├── Program.cs        ← entry point (top-level or Main method)
├── appsettings.json  ← config (web apps)
└── bin/ obj/         ← build output (ignore in git)
1.3

NuGet Packages

Libraries you add with one command — like npm for .NET.

// Add a package:
// dotnet add package Microsoft.EntityFrameworkCore.SqlServer

// In .csproj it appears as:
// <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />

using Microsoft.EntityFrameworkCore; // now available in code
02

Types, Variables & Memory

C# is statically typed — every variable has a type. The compiler catches mistakes before you run anything. That's a feature, not a limitation.
2.1

Value vs Reference Types

int, bool, double live on the stack (copied). string, class, array live on the heap (referenced).

int a = 10;
int b = a;   // b gets a COPY — changing b doesn't affect a
b = 20;
Console.WriteLine(a); // 10

int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1;      // arr2 points to SAME array
arr2[0] = 99;
Console.WriteLine(arr1[0]); // 99 — surprise if you didn't know!
2.2

Common Types Cheat Sheet

int age = 30;
long big = 9_000_000_000L;
double price = 19.99;
decimal money = 19.99m;   // use decimal for money!
bool active = true;
char letter = 'A';
string name = "Spyros";
DateTime now = DateTime.UtcNow;
Guid id = Guid.NewGuid();

// var — compiler infers the type
var city = "Athens";      // string
var count = 42;           // int
2.3

Nullable Types

Value types that can be null. Essential for databases and APIs.

int? maybeAge = null;           // nullable int
string? nickname = null;        // nullable reference (C# 8+)

// Null-coalescing
int age = maybeAge ?? 0;        // 0 if null
string display = nickname ?? "Anonymous";

// Null-conditional
int? len = nickname?.Length;    // null if nickname is null

// Pattern matching null check
if (nickname is not null)
    Console.WriteLine(nickname.Length);
2.4

Strings & StringBuilder

// Interpolation — preferred
var msg = $"Hello, {name}! You have {count} messages.";

// Raw string literals (.NET 7+) — great for SQL, JSON, regex
var sql = """
    SELECT Id, Name
    FROM Customers
    WHERE Active = 1
    """;

// StringBuilder — use in loops (strings are immutable!)
var sb = new System.Text.StringBuilder();
foreach (var item in items)
    sb.AppendLine(item);
var result = sb.ToString();
03

Control Flow & Methods

3.1

Conditions & Switch Expressions

// Classic if/else
if (score >= 90) grade = "A";
else if (score >= 80) grade = "B";
else grade = "C";

// Switch expression — modern, returns a value
var label = status switch
{
    "active"   => "Running",
    "paused"   => "On hold",
    "archived" => "Done",
    _          => "Unknown"   // default
};

// Pattern matching
if (obj is string { Length: > 0 } s)
    Console.WriteLine(s);
3.2

Loops

// foreach — use for collections (preferred)
foreach (var item in items)
    Console.WriteLine(item);

// for — when you need the index
for (int i = 0; i < items.Count; i++)
    Console.WriteLine($"{i}: {items[i]}");

// while / do-while
while (queue.Count > 0)
    Process(queue.Dequeue());

// LINQ alternative — often cleaner
items.ForEach(i => Console.WriteLine(i));
3.3

Methods — Parameters & Returns

// Basic method
static int Add(int a, int b) => a + b;

// Optional parameters
static void Greet(string name, string greeting = "Hello")
    => Console.WriteLine($"{greeting}, {name}!");

// out parameter — returns multiple values
static bool TryParse(string input, out int result)
{
    return int.TryParse(input, out result);
}

// ref vs out: ref must be initialized before passing

// params — variable argument count
static int Sum(params int[] numbers)
    => numbers.Sum();
3.4

Exception Handling

try
{
    var data = File.ReadAllText("missing.txt");
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"File not found: {ex.FileName}");
}
catch (Exception ex) when (ex.Message.Contains("access"))
{
    Console.WriteLine("Permission denied.");
}
finally
{
    // always runs — cleanup connections, files, etc.
}

// Throw your own
throw new InvalidOperationException("Order already shipped.");
04

OOP — Classes, Records & Interfaces

Brain Power

What's the difference between a class and a record? Guess before reading 4.2.

Hint: records are optimised for immutable data — great for DTOs and API responses.

4.1

Classes & Properties

public class Customer
{
    // Auto-property with default
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; private set; } = string.Empty;

    // Constructor
    public Customer(string name, string email)
    {
        Name = name;
        Email = email;
    }

    // Method
    public string GetLabel() => $"{Name} <{Email}>";
}

var c = new Customer("Spyros", "sponaris@gmail.com");
4.2

Records — Immutable Data

// Positional record — concise DTO
public record ProductDto(int Id, string Name, decimal Price);

var p = new ProductDto(1, "Widget", 9.99m);
var copy = p with { Price = 8.99m }; // non-destructive mutation

// Record classes — reference type with value equality
public record Order(int Id, DateTime Created);

Console.WriteLine(p == copy); // false — different Price
4.3

Inheritance & Polymorphism

public abstract class Animal
{
    public abstract string Speak();
    public virtual void Sleep() => Console.WriteLine("Zzz...");
}

public class Dog : Animal
{
    public override string Speak() => "Woof!";
}

public class Cat : Animal
{
    public override string Speak() => "Meow!";
}

// Polymorphism — treat all as Animal
Animal[] pets = [new Dog(), new Cat()];
foreach (var pet in pets)
    Console.WriteLine(pet.Speak());
4.4

Interfaces & Dependency Injection

Program to interfaces, not implementations. This is how ASP.NET Core DI works.

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}

public class SmtpEmailSender : IEmailSender
{
    public Task SendAsync(string to, string subject, string body)
    {
        // real SMTP logic here
        return Task.CompletedTask;
    }
}

// Register in ASP.NET Core:
// builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

// Inject via constructor:
public class OrderService(IEmailSender email)
{
    public async Task ConfirmAsync(Order order)
        => await email.SendAsync(order.Email, "Confirmed", "Thanks!");
}
05

Collections & LINQ

5.1

Collection Types — When to Use What

// List<T> — default choice, ordered, indexable
List<string> names = ["Alice", "Bob", "Carol"];

// Dictionary<K,V> — key → value lookup
Dictionary<int, string> users = new()
{
    [1] = "Alice", [2] = "Bob"
};

// HashSet<T> — unique items, fast Contains()
HashSet<int> ids = [1, 2, 3, 3]; // {1, 2, 3}

// Queue<T> — FIFO (first in, first out)
Queue<string> queue = new();
queue.Enqueue("first"); queue.Dequeue();

// Stack<T> — LIFO (last in, first out)
Stack<string> stack = new();
stack.Push("bottom"); stack.Pop();

// SortedList, LinkedList, ImmutableArray — specialised cases
5.2

LINQ — Query Your Data

var products = new[]
{
    new { Name = "Laptop",  Price = 999m,  Cat = "Tech" },
    new { Name = "Mouse",   Price = 29m,   Cat = "Tech" },
    new { Name = "Desk",    Price = 450m,  Cat = "Office" },
};

// Method syntax (most common in production)
var expensive = products
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Price)
    .Select(p => p.Name)
    .ToList();

// Query syntax (SQL-like)
var tech = from p in products
           where p.Cat == "Tech"
           orderby p.Price
           select p.Name;

// Aggregations
var total = products.Sum(p => p.Price);
var avg   = products.Average(p => p.Price);
var any   = products.Any(p => p.Price > 500);
var first = products.FirstOrDefault(p => p.Name == "Laptop");
5.3

IEnumerable vs IQueryable

IEnumerable runs in memory. IQueryable translates to SQL when used with EF Core.

// IEnumerable — filters in C# memory (after DB fetch)
IEnumerable<Customer> all = db.Customers.ToList();
var filtered = all.Where(c => c.Name.StartsWith("A"));

// IQueryable — EF Core translates to SQL (efficient!)
IQueryable<Customer> query = db.Customers;
var sqlFiltered = query.Where(c => c.Name.StartsWith("A"));
// Generated SQL: SELECT ... WHERE Name LIKE 'A%'
06

Concurrent Collections

Regular List<T> and Dictionary<K,V> are not thread-safe. If two threads write at the same time, you get corrupted data or crashes.

System.Collections.Concurrent gives you thread-safe versions designed for multi-threaded and async scenarios. Use them whenever multiple threads share a collection.

Watch Out

Don't wrap a regular Dictionary in lock {} unless you have to. Use ConcurrentDictionary instead — it's built for this.

6.1

ConcurrentDictionary<K,V>

Thread-safe dictionary. The most-used concurrent collection in production .NET.

using System.Collections.Concurrent;

var cache = new ConcurrentDictionary<string, string>();

// TryAdd — only adds if key doesn't exist
cache.TryAdd("user:1", "Alice");
cache.TryAdd("user:1", "Bob");   // false — key exists

// GetOrAdd — fetch or create atomically
var value = cache.GetOrAdd("user:2", key => LoadFromDb(key));

// AddOrUpdate — update if exists, add if not
cache.AddOrUpdate("user:1",
    addValue: "New",
    updateFactory: (key, old) => old + " (updated)");

// TryRemove / TryGetValue — safe concurrent access
if (cache.TryGetValue("user:1", out var name))
    Console.WriteLine(name);
6.2

ConcurrentBag, Queue & Stack

// ConcurrentBag<T> — unordered, thread-safe add/take
var bag = new ConcurrentBag<int>();
Parallel.For(0, 100, i => bag.Add(i));
while (bag.TryTake(out int item))
    Process(item);

// ConcurrentQueue<T> — FIFO, thread-safe
var queue = new ConcurrentQueue<WorkItem>();
queue.Enqueue(new WorkItem("job-1"));
if (queue.TryDequeue(out var job))
    await ProcessAsync(job);

// ConcurrentStack<T> — LIFO, thread-safe
var stack = new ConcurrentStack<string>();
stack.Push("frame-3");
stack.TryPop(out string? frame);
6.3

BlockingCollection<T> — Producer/Consumer

Bounded buffer between producers and consumers. Blocks when full or empty.

var buffer = new BlockingCollection<string>(boundedCapacity: 100);

// Producer
Task.Run(() =>
{
    for (int i = 0; i < 1000; i++)
        buffer.Add($"item-{i}");
    buffer.CompleteAdding();
});

// Consumer
Task.Run(() =>
{
    foreach (var item in buffer.GetConsumingEnumerable())
        Console.WriteLine(item);
});

// Also supports TryAdd with timeout:
buffer.TryAdd("urgent", TimeSpan.FromMilliseconds(50));
6.4

Real-World: In-Memory Cache

public class ThreadSafeCache<TKey, TValue> where TKey : notnull
{
    private readonly ConcurrentDictionary<TKey, CacheEntry<TValue>> _store = new();

    public TValue GetOrCreate(TKey key, Func<TValue> factory, TimeSpan ttl)
    {
        var entry = _store.GetOrAdd(key, _ => new CacheEntry<TValue>(factory(), ttl));
        if (entry.IsExpired)
        {
            _store.TryRemove(key, out _);
            entry = _store.GetOrAdd(key, _ => new CacheEntry<TValue>(factory(), ttl));
        }
        return entry.Value;
    }
}

record CacheEntry<T>(T Value, TimeSpan Ttl)
{
    public DateTime Created { get; } = DateTime.UtcNow;
    public bool IsExpired => DateTime.UtcNow - Created > Ttl;
}
07

Async/Await & Threading

async/await lets your app do other work while waiting for I/O (database, HTTP, files). It does not create a new thread for every call — it's efficient. For the full guide — ThreadPool, state machine, Channels, deadlocks — see Threading & Multitasking In Depth.
7.1

async/await Basics

// Always return Task or Task<T> from async methods
public async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    var response = await client.GetAsync(url);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

// Call it
var json = await FetchDataAsync("https://api.example.com/data");

// Parallel — run multiple tasks at once
var task1 = FetchDataAsync(url1);
var task2 = FetchDataAsync(url2);
await Task.WhenAll(task1, task2);
7.2

Common Mistakes

// ❌ BAD — blocks the thread, can cause deadlocks
var result = FetchDataAsync(url).Result;
var result2 = FetchDataAsync(url).GetAwaiter().GetResult();

// ✅ GOOD — await all the way
var result = await FetchDataAsync(url);

// ❌ BAD — async void (only allowed in event handlers)
public async void Process() { ... }

// ✅ GOOD
public async Task ProcessAsync() { ... }

// ❌ BAD — unnecessary async when no await
public async Task<int> GetValueAsync() => 42; // just return Task.FromResult(42)

// ConfigureAwait(false) in library code (not ASP.NET Core controllers)
await SomeLibraryMethodAsync().ConfigureAwait(false);
7.3

CancellationToken

public async Task<List<Customer>> GetCustomersAsync(
    CancellationToken ct = default)
{
    ct.ThrowIfCancellationRequested();
    return await db.Customers.ToListAsync(ct);
}

// In ASP.NET Core, CancellationToken is injected automatically:
app.MapGet("/customers", async (AppDbContext db, CancellationToken ct) =>
{
    var list = await db.Customers.ToListAsync(ct);
    return Results.Ok(list);
});

// Timeout with CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await LongRunningOpAsync(cts.Token);
7.4

Parallel & Channels

// Parallel.ForEachAsync (.NET 6+) — bounded parallelism
await Parallel.ForEachAsync(urls, new ParallelOptions
{
    MaxDegreeOfParallelism = 4
}, async (url, ct) =>
{
    await FetchAndSaveAsync(url, ct);
});

// System.Threading.Channels — modern producer/consumer
var channel = Channel.CreateBounded<WorkItem>(100);
var writer = channel.Writer;
var reader = channel.Reader;

// Producer
await writer.WriteAsync(new WorkItem("job-1"));

// Consumer
await foreach (var item in reader.ReadAllAsync())
    await ProcessAsync(item);
08

Generics, Delegates & Modern C#

8.1

Generics

// Generic class — works with any type
public class Repository<T> where T : class
{
    private readonly List<T> _items = new();
    public void Add(T item) => _items.Add(item);
    public T? Find(Func<T, bool> predicate)
        => _items.FirstOrDefault(predicate);
}

// Generic method
public static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) > 0 ? a : b;

// Constraints: where T : class, new(), IEntity, struct
8.2

Delegates, Func & Action

// Func<T, TResult> — takes params, returns a value
Func<int, int, int> add = (a, b) => a + b;

// Action<T> — takes params, returns void
Action<string> log = msg => Console.WriteLine(msg);

// Predicate<T> — returns bool
Predicate<int> isEven = n => n % 2 == 0;

// Events — built on delegates
public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;

protected virtual void OnOrderPlaced(Order order)
    => OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(order));
8.3

Modern C# Features (9–13)

// Primary constructors (C# 12)
public class OrderService(AppDbContext db, ILogger<OrderService> log)
{
    public async Task<Order?> GetAsync(int id)
        => await db.Orders.FindAsync(id);
}

// Collection expressions (C# 12)
int[] nums = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob"];

// Raw string literals (C# 11)
var json = """{ "name": "Spyros", "role": "Tech Lead" }""";

// Required members (C# 11)
public class User
{
    public required string Email { get; set; }
}

// file-scoped namespace (less indentation)
namespace MyApp.Services;
09

ASP.NET Core Essentials

ASP.NET Core is the web framework for .NET. You can build REST APIs (Minimal APIs or Controllers), Blazor web apps, gRPC services, SignalR real-time apps — all in one project.

9.1

Minimal API — Your First Endpoint

// dotnet new web -n MyApi
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

var app = builder.Build();

app.MapGet("/products", async (AppDbContext db) =>
    await db.Products.ToListAsync());

app.MapGet("/products/{id:int}", async (int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product p
        ? Results.Ok(p)
        : Results.NotFound());

app.MapPost("/products", async (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Results.Created($"/products/{product.Id}", product);
});

app.Run();
9.2

Middleware Pipeline

Every HTTP request passes through middleware in order. Think of it as a chain of filters.

var app = builder.Build();

// Order matters!
app.UseHttpsRedirection();
app.UseAuthentication();    // who are you?
app.UseAuthorization();     // what can you do?
app.UseExceptionHandler();  // catch errors → ProblemDetails

// Custom middleware
app.Use(async (context, next) =>
{
    var sw = Stopwatch.StartNew();
    await next(context);
    sw.Stop();
    context.Response.Headers["X-Elapsed-Ms"] = sw.ElapsedMilliseconds.ToString();
});

app.MapControllers();
app.Run();
9.3

Configuration & Dependency Injection

// appsettings.json
// { "ConnectionStrings": { "Default": "Server=..." } }

// Bind to a typed options class
builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection("Email"));

// Register services by lifetime:
builder.Services.AddSingleton<ICache, MemoryCache>();   // one instance
builder.Services.AddScoped<IOrderService, OrderService>(); // per request
builder.Services.AddTransient<IValidator, OrderValidator>(); // every injection

// Inject anywhere via constructor
public class ProductsController(IProductService products) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll()
        => Ok(await products.GetAllAsync());
}
9.4

Validation & ProblemDetails

// FluentValidation
public class CreateProductValidator : AbstractValidator<CreateProductDto>
{
    public CreateProductValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Price).GreaterThan(0);
    }
}

// Register
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductValidator>();

// Global exception handler (.NET 8+)
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

// Returns RFC 7807 JSON on errors:
// { "type": "...", "title": "Validation Error", "status": 400, "errors": {...} }
10

ASP.NET Core Identity

ASP.NET Core Identity is the built-in membership system: user accounts, passwords, roles, claims, external logins (Google, Microsoft), two-factor auth, and token providers. It stores everything in your database via EF Core.

→ Read the full in-depth Identity tutorial for UserManager, JWT Bearer, refresh tokens, authorization policies, 2FA, and production hardening.

10.1

Setup Identity with EF Core

// Packages:
// Microsoft.AspNetCore.Identity.EntityFrameworkCore
// Microsoft.EntityFrameworkCore.SqlServer

public class AppDbContext : IdentityDbContext<ApplicationUser>
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<Product> Products => Set<Product>();
}

public class ApplicationUser : IdentityUser
{
    public string? FullName { get; set; }
}

// Program.cs
builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseSqlServer(connString));

builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.Password.RequiredLength = 8;
    options.Password.RequireDigit = true;
    options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();

// Migrations: dotnet ef migrations add AddIdentity
//             dotnet ef database update
10.2

Register & Login Endpoints

app.MapPost("/auth/register", async (
    RegisterDto dto,
    UserManager<ApplicationUser> userManager) =>
{
    var user = new ApplicationUser
    {
        UserName = dto.Email,
        Email = dto.Email,
        FullName = dto.FullName
    };
    var result = await userManager.CreateAsync(user, dto.Password);
    return result.Succeeded
        ? Results.Ok(new { message = "Registered" })
        : Results.BadRequest(result.Errors);
});

app.MapPost("/auth/login", async (
    LoginDto dto,
    SignInManager<ApplicationUser> signInManager) =>
{
    var result = await signInManager.PasswordSignInAsync(
        dto.Email, dto.Password, isPersistent: false, lockoutOnFailure: true);
    return result.Succeeded
        ? Results.Ok(new { message = "Logged in" })
        : Results.Unauthorized();
});
10.3

JWT Bearer Authentication

For SPAs and mobile apps, use JWT tokens instead of cookies.

// Package: Microsoft.AspNetCore.Authentication.JwtBearer

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

// Generate a token after login
var token = new JwtSecurityToken(
    issuer: config["Jwt:Issuer"],
    audience: config["Jwt:Audience"],
    claims: new[] { new Claim(ClaimTypes.Name, user.Email!) },
    expires: DateTime.UtcNow.AddHours(1),
    signingCredentials: creds);
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
10.4

Roles, Claims & Authorization

// Assign a role
await userManager.AddToRoleAsync(user, "Admin");

// Role-based authorization
app.MapGet("/admin/users", async (UserManager<ApplicationUser> um) =>
    await um.Users.ToListAsync())
.RequireAuthorization("AdminOnly");

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
    options.AddPolicy("CanEditProducts", p =>
        p.RequireClaim("permission", "products:edit"));
});

// In Minimal API / Controllers
app.MapDelete("/products/{id}", DeleteProduct)
   .RequireAuthorization("CanEditProducts");

// Get current user in endpoint
app.MapGet("/me", (ClaimsPrincipal user) =>
    Results.Ok(user.Identity?.Name));
11

SQL Server & Entity Framework Core

Entity Framework Core (EF Core) is the ORM — you work with C# objects, EF translates to SQL. SQL Server is the database. Together they're the default data stack for .NET enterprise apps.

For raw SQL depth — including database design, normalization and indexing — see the dedicated T-SQL Deep-Dive (8 sections).

11.1

Entities & DbContext

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; } = null!;
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();

    protected override void OnModelCreating(ModelBuilder model)
    {
        model.Entity<Product>(e =>
        {
            e.Property(p => p.Price).HasPrecision(18, 2);
            e.HasIndex(p => p.Name);
        });
    }
}
11.2

CRUD Operations

// CREATE
var product = new Product { Name = "Widget", Price = 9.99m };
db.Products.Add(product);
await db.SaveChangesAsync();

// READ
var all = await db.Products.Include(p => p.Category).ToListAsync();
var one = await db.Products.FirstOrDefaultAsync(p => p.Id == id);

// UPDATE
product.Price = 12.99m;
db.Products.Update(product);  // or just modify — EF tracks changes
await db.SaveChangesAsync();

// DELETE
db.Products.Remove(product);
await db.SaveChangesAsync();

// AsNoTracking — read-only queries (faster, no change tracking)
var snapshot = await db.Products.AsNoTracking().ToListAsync();
11.3

Migrations & Raw SQL

// Create migration after model changes
// dotnet ef migrations add AddProductsTable
// dotnet ef database update

// Raw SQL when you need performance
var top = await db.Products
    .FromSqlRaw("SELECT TOP 10 * FROM Products ORDER BY Price DESC")
    .ToListAsync();

// Parameterised — always use parameters, never string concat!
var results = await db.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice}")
    .ToListAsync();

// Execute raw command
await db.Database.ExecuteSqlRawAsync(
    "UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = {0}", catId);
11.4

Relationships & Performance

// Eager loading — one query with JOIN
var orders = await db.Orders
    .Include(o => o.Items)
    .ThenInclude(i => i.Product)
    .Where(o => o.CustomerId == id)
    .ToListAsync();

// Projection — select only what you need (fastest)
var dtos = await db.Products
    .Select(p => new ProductDto(p.Id, p.Name, p.Price))
    .ToListAsync();

// Avoid N+1: always Include or project
// ❌ foreach order → order.Items (N extra queries)
// ✅ Include(o => o.Items) upfront

// Compiled queries — cache the query plan
private static readonly Func<AppDbContext, int, Task<Product?>> GetById =
    EF.CompileAsyncQuery((AppDbContext db, int id) =>
        db.Products.FirstOrDefault(p => p.Id == id));
12

GraphQL with Hot Chocolate

REST gives you fixed endpoints. GraphQL lets the client ask for exactly the fields it needs in one request. In ASP.NET Core, Hot Chocolate is the go-to GraphQL server.

When to use GraphQL: complex frontends (React, mobile) that need flexible data fetching. When to use REST: simple CRUD, public APIs, caching at CDN level.

12.1

Setup Hot Chocolate

// dotnet add package HotChocolate.AspNetCore
// dotnet add package HotChocolate.Data.EntityFramework

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o =>
    o.UseSqlServer(connString));

builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddProjections()
    .AddFiltering()
    .AddSorting()
    .RegisterDbContext<AppDbContext>(DbContextKind.Pooled);

var app = builder.Build();
app.MapGraphQL();  // endpoint: /graphql
app.Run();
12.2

Query Type

public class Query
{
    // List all products
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<Product> GetProducts(AppDbContext db)
        => db.Products;

    // Single product by ID
    public async Task<Product?> GetProductById(
        int id, AppDbContext db)
        => await db.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);
}

// Client query — ask for only the fields you need:
// query {
//   products(where: { price: { gt: 10 } }, order: { name: ASC }) {
//     id
//     name
//     price
//     category { name }
//   }
// }
12.3

Mutations — Create, Update, Delete

public class Mutation
{
    public async Task<Product> CreateProduct(
        CreateProductInput input, AppDbContext db)
    {
        var product = new Product
        {
            Name = input.Name,
            Price = input.Price,
            CategoryId = input.CategoryId
        };
        db.Products.Add(product);
        await db.SaveChangesAsync();
        return product;
    }

    public async Task<bool> DeleteProduct(int id, AppDbContext db)
    {
        var product = await db.Products.FindAsync(id);
        if (product is null) return false;
        db.Products.Remove(product);
        await db.SaveChangesAsync();
        return true;
    }
}

// Client mutation:
// mutation {
//   createProduct(input: { name: "Widget", price: 9.99, categoryId: 1 }) {
//     id
//     name
//   }
// }
12.4

Auth, Subscriptions & Banana Cake Pop

// Secure GraphQL with JWT
builder.Services.AddGraphQLServer()
    .AddAuthorization()
    .AddQueryType<Query>();

// On a field or class:
[Authorize(Roles = "Admin")]
public IQueryable<User> GetUsers(AppDbContext db) => db.Users;

// Subscriptions — real-time updates via WebSocket
public class Subscription
{
    [Subscribe]
    [Topic("ProductCreated")]
    public Product OnProductCreated(
        [EventMessage] Product product) => product;
}

// Publish from mutation:
await sender.SendAsync("ProductCreated", product);

// Dev tool: Hot Chocolate includes Banana Cake Pop at /graphql
// Use it to explore schema, run queries, test mutations
Mastery Move — where to go next

You've now covered the full .NET stack in one page. Natural next steps:

1. OOP & SOLID for Dummies — turn the class basics into proper design with SOLID principles.

2. REST API Best Practices — pagination, HATEOAS, versioning, production-grade APIs.

3. T-SQL Deep-Dive — window functions, indexes, stored procedures, query tuning.

4. C# Deep-Dive — Span<T>, expression trees, source generators, advanced performance.

5. .NET How-To Cookbook — 25+ copy-paste recipes for everyday problems.