Learnixo
Back to blog
Backend Systemsbeginner

C# 12 and .NET 8: Modern Cross-Platform Development Fundamentals

A complete guide inspired by Mark J. Price's bestselling book. Covers C# 12 features (primary constructors, type aliasing, collection expressions), .NET 8 AOT, OOP, pattern matching, Blazor, ASP.NET Core, EF Core, LINQ, and deployment.

LearnixoJune 4, 202612 min read
.NETC#C# 12.NET 8ASP.NET CoreBlazorEF CoreLINQ
Share:š•

About This Guide

"C# 12 and .NET 8: Modern Cross-Platform Development Fundamentals" by Mark J. Price (Packt, 8th Edition) is an Amazon bestseller and one of the most comprehensive beginner-to-intermediate .NET books available.

Mark J. Price is a seasoned Software Engineer and former Microsoft Certified Trainer (MCT) with 20+ years of experience, who helped prepare Microsoft's own learning materials for .NET certification.

This guide walks through the book's key topics with practical code for every concept.


Part 1: C# Language Fundamentals

Variables and Types

C#
// Value types — stored on stack
int    age      = 28;
double price    = 19.99;
decimal total   = 1234.56m;   // m suffix — exact for money
bool   isActive = true;
char   grade    = 'A';

// Reference types — stored on heap
string name    = "Mark";
object anything = 42;

// Implicit typing — compiler infers the type
var count  = 10;      // int
var text   = "Hello"; // string
var items  = new List<string>(); // List<string>

// Nullable reference types (C# 8+)
string?  maybeNull = null;  // explicitly nullable
string   notNull   = "Hi";  // compiler warns if null assigned

String Interpolation and Raw String Literals (C# 11+)

C#
string name = "Alice";
int    age  = 30;

// String interpolation
string message = $"Hello {name}, you are {age} years old.";

// Verbatim string — backslashes treated literally
string path = @"C:\Users\Alice\Documents\file.txt";

// Raw string literals (C# 11) — no escaping needed
string json = """
    {
        "name": "Alice",
        "age": 30,
        "path": "C:\Users\Alice"
    }
    """;

// Interpolated raw string
string html = $"""
    <h1>Hello, {name}!</h1>
    <p>You are {age} years old.</p>
    """;

Indexes and Ranges (C# 8+)

C#
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Index from end — ^ operator
int last        = numbers[^1];  // 10
int secondLast  = numbers[^2];  // 9

// Range — .. operator
int[] first3    = numbers[..3];     // 1, 2, 3
int[] last3     = numbers[^3..];    // 8, 9, 10
int[] middle    = numbers[2..5];    // 3, 4, 5
int[] all       = numbers[..];      // whole array

string name = "Alice Smith";
string first = name[..5];   // "Alice"
string last2 = name[6..];   // "Smith"

Part 2: Object-Oriented Programming

Classes and Encapsulation

C#
public class BankAccount
{
    // Private field — encapsulated
    private decimal _balance;

    // Public properties — controlled access
    public string AccountNumber { get; }
    public string Owner         { get; }
    public decimal Balance      => _balance; // read-only

    // Constructor
    public BankAccount(string accountNumber, string owner, decimal initialBalance = 0)
    {
        AccountNumber = accountNumber;
        Owner         = owner;
        _balance      = initialBalance;
    }

    // Methods enforce business rules
    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Deposit must be positive.", nameof(amount));
        _balance += amount;
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Withdrawal must be positive.", nameof(amount));
        if (amount > _balance)
            throw new InvalidOperationException("Insufficient funds.");
        _balance -= amount;
    }
}

Inheritance and Polymorphism

C#
// Base class
public abstract class Shape
{
    public string Color { get; set; } = "White";
    public abstract double Area();            // must be implemented
    public virtual  string Describe()        // can be overridden
        => $"A {Color} shape with area {Area():F2}";
}

// Derived classes
public class Circle : Shape
{
    public double Radius { get; }
    public Circle(double radius) => Radius = radius;
    public override double Area() => Math.PI * Radius * Radius;
}

public class Rectangle : Shape
{
    public double Width { get; }
    public double Height { get; }
    public Rectangle(double width, double height)
    {
        Width  = width;
        Height = height;
    }
    public override double Area() => Width * Height;
    public override string Describe() => $"Rectangle {Width}Ɨ{Height}, area {Area():F2}";
}

// Polymorphism
Shape[] shapes = { new Circle(5), new Rectangle(4, 6) };
foreach (var shape in shapes)
    Console.WriteLine(shape.Describe()); // calls the correct override

Interfaces

C#
public interface IDrawable
{
    void Draw();
    string GetDescription() => $"Drawable: {GetType().Name}"; // default implementation
}

public interface IPrintable
{
    void Print();
}

// A class can implement multiple interfaces
public class Circle : Shape, IDrawable, IPrintable
{
    public void Draw()  => Console.WriteLine($"Drawing circle r={Radius}");
    public void Print() => Console.WriteLine($"Printing circle r={Radius}");
}

Part 3: Modern C# Features

Records and Record Structs (C# 9–10)

C#
// Record — reference type with value equality, immutable by default
public record Person(string FirstName, string LastName, int Age);

var alice = new Person("Alice", "Smith", 30);
var alice2 = new Person("Alice", "Smith", 30);

Console.WriteLine(alice == alice2);  // true — value equality
Console.WriteLine(alice);           // Person { FirstName = Alice, LastName = Smith, Age = 30 }

// Non-destructive mutation with 'with'
var olderAlice = alice with { Age = 31 };

// Record struct (C# 10) — value type record
public record struct Point(double X, double Y);

// Positional record with additional members
public record Order(Guid Id, string CustomerId, decimal Total)
{
    public bool IsHighValue => Total > 1000;
}

Primary Constructors (C# 12)

C#
// C# 12 — primary constructor parameters available throughout the class
public class OrderService(IOrderRepository orders, IEmailService email)
{
    // Parameters are in scope for the whole class — no need to store in fields explicitly
    public async Task<Order?> GetOrderAsync(Guid id, CancellationToken ct)
        => await orders.GetByIdAsync(id, ct);

    public async Task ConfirmOrderAsync(Guid id, CancellationToken ct)
    {
        var order = await orders.GetByIdAsync(id, ct)
            ?? throw new NotFoundException("Order", id);

        await email.SendConfirmationAsync(order.CustomerId, order.Id, ct);
    }
}

// Also works on structs and records
public class Point(double x, double y)
{
    public double X { get; } = x;
    public double Y { get; } = y;
    public double Distance => Math.Sqrt(X * X + Y * Y);
}

Type Aliasing (C# 12)

C#
// C# 12 — alias any type, not just simple types
using OrderId      = System.Guid;
using CustomerId   = System.Guid;
using OrderLines   = System.Collections.Generic.List<OrderLine>;
using HttpHeaders  = System.Collections.Generic.Dictionary<string, string>;

// Makes intent clearer — OrderId and CustomerId are both Guid but semantically different
public async Task<Order> GetOrderAsync(OrderId orderId, CustomerId customerId)
{ /* ... */ }

Pattern Matching

C#
// Switch expression (C# 8+)
string GetDiscount(object customer) => customer switch
{
    PremiumCustomer p when p.YearsAsCustomer > 5 => "20%",
    PremiumCustomer                               => "10%",
    StandardCustomer { LoyaltyPoints: > 1000 }   => "5%",
    StandardCustomer                              => "0%",
    null                                          => throw new ArgumentNullException(nameof(customer)),
    _                                             => "0%"
};

// Property pattern
bool IsEligibleForShipping(Order order) => order is
{
    Status:   OrderStatus.Submitted,
    Total: >= 50,
    Items: { Count: > 0 }
};

// List pattern (C# 11)
string DescribeList(int[] nums) => nums switch
{
    []          => "Empty",
    [var single]=> $"Single: {single}",
    [var first, var second] => $"Two items: {first}, {second}",
    [var first, .., var last] => $"Starts {first}, ends {last}"
};

Switch Expressions

C#
// Replaces verbose switch statements
decimal GetTaxRate(string country) => country switch
{
    "UK"  => 0.20m,
    "DE"  => 0.19m,
    "US"  => 0.08m,
    "AU"  => 0.10m,
    _     => throw new NotSupportedException($"Tax rate for {country} not configured")
};

// With when guards
string ClassifyAge(int age) => age switch
{
    < 0          => throw new ArgumentException("Age cannot be negative"),
    < 13         => "Child",
    < 18         => "Teenager",
    < 65         => "Adult",
    >= 65        => "Senior"
};

// Returns a value — can be used inline
var tax = GetTaxRate(order.Country) * order.Total;

Collection Expressions (C# 12)

C#
// Unified syntax for all collection types
int[]        array   = [1, 2, 3];
List<int>    list    = [1, 2, 3];
Span<int>    span    = [1, 2, 3];
ImmutableArray<string> immutable = ["a", "b", "c"];

// Spread operator
int[] first  = [1, 2, 3];
int[] second = [4, 5, 6];
int[] all    = [..first, ..second, 7, 8];  // [1,2,3,4,5,6,7,8]

Part 4: LINQ

C#
var orders = new List<Order> { /* ... */ };

// Filtering
var pending = orders.Where(o => o.Status == OrderStatus.Pending);

// Projection
var summaries = orders.Select(o => new
{
    o.Id,
    o.Total,
    LineCount = o.Lines.Count
});

// Ordering
var sorted = orders.OrderByDescending(o => o.Total)
                   .ThenBy(o => o.CreatedAt);

// Grouping
var byStatus = orders
    .GroupBy(o => o.Status)
    .Select(g => new { Status = g.Key, Count = g.Count(), Total = g.Sum(o => o.Total) });

// Aggregation
decimal totalRevenue = orders.Where(o => o.Status == OrderStatus.Delivered).Sum(o => o.Total);
int     avgItems     = (int)orders.Average(o => o.Lines.Count);
Order?  largest      = orders.MaxBy(o => o.Total);

// Any / All / Count
bool hasOverdue = orders.Any(o => o.DueDate < DateTime.UtcNow && o.Status != OrderStatus.Delivered);
bool allShipped = orders.All(o => o.Status >= OrderStatus.Shipped);

// Flattening
var allLines = orders.SelectMany(o => o.Lines);

// Query syntax (alternative)
var result = from o in orders
             where o.Total > 100
             orderby o.CreatedAt descending
             select new { o.Id, o.Total };

Part 5: ASP.NET Core Web API

C#
// Program.cs — minimal API
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.MapControllers();
app.Run();

// Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _db;
    public ProductsController(AppDbContext db) => _db = db;

    [HttpGet]
    public async Task<IActionResult> GetAll(CancellationToken ct)
        => Ok(await _db.Products.ToListAsync(ct));

    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetById(int id, CancellationToken ct)
    {
        var product = await _db.Products.FindAsync(id, ct);
        return product is null ? NotFound() : Ok(product);
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateProductRequest req, CancellationToken ct)
    {
        var product = new Product { Name = req.Name, Price = req.Price };
        _db.Products.Add(product);
        await _db.SaveChangesAsync(ct);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
}

Part 6: Blazor

RAZOR
@* Pages/Products.razor *@
@page "/products"
@inject HttpClient Http

<h1>Products</h1>

@if (_products is null)
{
    <p>Loading...</p>
}
else
{
    <table class="table">
        @foreach (var product in _products)
        {
            <tr>
                <td>@product.Name</td>
                <td>@product.Price.ToString("C")</td>
                <td><button @onclick="() => DeleteProduct(product.Id)">Delete</button></td>
            </tr>
        }
    </table>
}

@code {
    private List<Product>? _products;

    protected override async Task OnInitializedAsync()
        => _products = await Http.GetFromJsonAsync<List<Product>>("api/products");

    private async Task DeleteProduct(int id)
    {
        await Http.DeleteAsync($"api/products/{id}");
        _products?.RemoveAll(p => p.Id == id);
    }
}

Part 7: Entity Framework Core

C#
// DbContext
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.HasKey(p => p.Id);
            e.Property(p => p.Name).HasMaxLength(200).IsRequired();
            e.Property(p => p.Price).HasPrecision(10, 2);
            e.HasOne(p => p.Category).WithMany(c => c.Products).HasForeignKey(p => p.CategoryId);
        });
    }
}

// Queries
var products = await db.Products
    .Include(p => p.Category)
    .Where(p => p.Price > 10)
    .OrderBy(p => p.Name)
    .ToListAsync();

// Create
db.Products.Add(new Product { Name = "Widget", Price = 9.99m, CategoryId = 1 });
await db.SaveChangesAsync();

// Update
var product = await db.Products.FindAsync(id);
product!.Price = 14.99m;
await db.SaveChangesAsync();

// Delete
db.Products.Remove(product!);
await db.SaveChangesAsync();

// Migrations
// dotnet ef migrations add InitialCreate
// dotnet ef database update

Part 8: .NET 8 AOT (Ahead-of-Time Compilation)

XML
<!-- .csproj — enable Native AOT for an ASP.NET Core API -->
<PropertyGroup>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
C#
// AOT-compatible minimal API
var builder = WebApplication.CreateSlimBuilder(args); // slim = AOT-optimised

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapGet("/products", () => new[] { new Product(1, "Widget", 9.99m) });

app.Run();

// Source-generated JSON serializer — required for AOT (no runtime reflection)
[JsonSerializable(typeof(Product[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

record Product(int Id, string Name, decimal Price);
Bash
# Publish as native binary
dotnet publish -r linux-x64 -c Release
# Output: ~15MB native binary, startup < 50ms

Part 9: Working with Files, Streams, and Serialisers

Files

C#
// Write
await File.WriteAllTextAsync("output.txt", "Hello, .NET 8!");
await File.WriteAllLinesAsync("lines.txt", new[] { "line1", "line2", "line3" });

// Read
string content = await File.ReadAllTextAsync("output.txt");
string[] lines = await File.ReadAllLinesAsync("lines.txt");

// Stream-based (memory efficient for large files)
await using var writer = new StreamWriter("large.txt");
for (int i = 0; i < 100_000; i++)
    await writer.WriteLineAsync($"Line {i}");

await using var reader = new StreamReader("large.txt");
while (!reader.EndOfStream)
{
    var line = await reader.ReadLineAsync();
    // process each line without loading all into memory
}

JSON Serialisation

C#
// System.Text.Json — built-in, fast
var product = new Product(1, "Widget", 9.99m);

// Serialise
string json = JsonSerializer.Serialize(product, new JsonSerializerOptions
{
    WriteIndented            = true,
    PropertyNamingPolicy     = JsonNamingPolicy.CamelCase,
    DefaultIgnoreCondition   = JsonIgnoreCondition.WhenWritingNull
});

// Deserialise
var restored = JsonSerializer.Deserialize<Product>(json);

// Stream-based (no intermediate string allocation)
await JsonSerializer.SerializeAsync(stream, product);
var fromStream = await JsonSerializer.DeserializeAsync<Product>(stream);

Part 10: NuGet Packages

Bash
# Add a package
dotnet add package Newtonsoft.Json
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.*

# List installed packages
dotnet list package

# Update packages
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.*

# Remove a package
dotnet remove package Newtonsoft.Json

# Restore all packages
dotnet restore

Part 11: Deployment

Bash
# Publish for deployment
dotnet publish -c Release -o ./publish

# Self-contained (includes .NET runtime — no runtime needed on target machine)
dotnet publish -c Release -r linux-x64 --self-contained true -o ./publish

# Docker
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM base AS final
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]

Key Takeaways from the Book

| Topic | What You Learn | |---|---| | C# 12 Primary Constructors | Cleaner class definitions, fewer boilerplate fields | | C# 12 Type Aliasing | Semantic clarity for strongly-typed IDs | | Pattern Matching + Switch Expressions | Replace if-else chains with readable declarative code | | Records | Immutable DTOs and value objects with one line | | Indexes and Ranges | Slice arrays and strings without loops | | AOT Compilation | Production-ready .NET with fast startup and minimal footprint | | Blazor | Full-stack C# — no JavaScript required | | EF Core + LINQ | Database access that feels like C# collections | | Minimal API | Lean, high-performance HTTP endpoints |

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.