EngineeringSenior DeveloperTechnical Sessions

Session 02: C# Advanced Features (.NET 9)

Mục tiêu: Master các tính năng C# mới nhất, viết code expressive, performant, và type-safe hơn.


1. Pattern Matching Nâng Cao

Property Patterns (C# 8+)

csharp
// Nested property pattern
public decimal CalculateShipping(Order order) => order switch
{
    { Total: > 500, Customer.IsPremium: true } => 0m,
    { Total: > 500 } => 9.99m,
    { Weight: > 20, Destination.Country: "VN" } => 25m,
    { Weight: > 20 } => 50m,
    { Total: > 100 } => 15m,
    _ => 30m
};

List Patterns (C# 11)

csharp
// Match trên collections
public string AnalyzeRoute(int[] segments) => segments switch
{
    [] => "Empty route",
    [var single] => $"Direct: {single}",
    [var first, .., var last] => $"Route from {first} to {last}",
};

// Real-world: parse CSV header
public bool IsValidCsvHeader(string[] headers) => headers switch
{
    ["Id", "Name", "Price", ..] => true,           // bắt đầu đúng
    [_, _, _, .., "Total"] => true,                 // kết thúc bằng Total
    { Length: > 100 } => false,                     // quá nhiều columns
    _ => false
};

// Relational + Logical patterns
public string ClassifyTemperature(double temp) => temp switch
{
    < 0 => "Freezing",
    >= 0 and < 15 => "Cold",
    >= 15 and < 30 => "Comfortable",
    >= 30 and < 40 => "Hot",
    >= 40 => "Extreme",
    double.NaN => "Invalid"
};

Switch Expression với Tuples

csharp
// Multi-value pattern matching
public decimal CalculateTax(string country, string productType, decimal amount) =>
    (country, productType) switch
    {
        ("VN", "food") => amount * 0.05m,
        ("VN", "electronics") => amount * 0.10m,
        ("VN", _) => amount * 0.08m,
        ("US", "food") => 0m,
        ("US", _) => amount * 0.07m,
        (_, "luxury") => amount * 0.20m,
        _ => amount * 0.10m
    };

2. Records & Value Objects

Record Types (C# 9+)

csharp
// Record class - reference type, immutable by convention
public record OrderCreatedEvent(
    Guid OrderId,
    string CustomerId,
    decimal Total,
    DateTime CreatedAt
);

// Non-destructive mutation với "with"
var original = new OrderCreatedEvent(Guid.NewGuid(), "C001", 100m, DateTime.UtcNow);
var modified = original with { Total = 150m };
// original.Total vẫn = 100m

// Record struct (C# 10) - value type, stack-allocated
public readonly record struct Money(decimal Amount, string Currency)
{
    // Tự có: Value equality, ToString(), Deconstruct()
    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new InvalidOperationException("Currency mismatch");
        return a with { Amount = a.Amount + b.Amount };
    }
}

// DDD Value Object pattern với record
public record Address
{
    public string Street { get; init; } = null!;
    public string City { get; init; } = null!;
    public string PostalCode { get; init; } = null!;

    // Custom validation trong constructor
    public Address
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(Street);
        ArgumentException.ThrowIfNullOrWhiteSpace(City);
    }
}

Record vs Class vs Struct - Khi Nào Chọn Gì?

Tính năngclassrecord classrecord structstruct
AllocationHeapHeapStackStack
EqualityReferenceValueValueValue (nếu override)
MutableCóCó thểCó thểCó
InheritanceCóRecord inheritanceKhôngKhông
with expressionKhôngCóCóKhông
Use caseDomain entitiesEvents, DTOsSmall value typesPerformance-critical

3. Primary Constructors & Required Members

Primary Constructors (C# 12)

csharp
// Class/Struct với Primary Constructor
public class OrderService(
    IOrderRepository repository,
    ILogger<OrderService> logger,
    TimeProvider timeProvider)
{
    // Parameters captured as private fields tự động
    // KHÔNG tạo public properties - chúng là captured variables

    public async Task<Order> CreateOrder(CreateOrderCommand cmd)
    {
        logger.LogInformation("Creating order for {CustomerId}", cmd.CustomerId);

        var order = Order.Create(cmd, timeProvider.GetUtcNow());
        await repository.AddAsync(order);
        return order;
    }
}

// ⚠️ Gotcha: Primary constructor params KHÔNG phải readonly
// Chúng có thể bị reassign bên trong class!
public class Counter(int initial)
{
    public int Current => initial;
    public void Increment() => initial++; // ⚠️ Mutate captured variable!
}

Required Members (C# 11)

csharp
public class CreateOrderRequest
{
    public required string CustomerId { get; init; }
    public required List<OrderItem> Items { get; init; }
    public string? CouponCode { get; init; }  // optional
}

// Compile error nếu thiếu required properties:
// ❌ var req = new CreateOrderRequest();
// ✅ var req = new CreateOrderRequest
// {
//     CustomerId = "C001",
//     Items = [new OrderItem("SKU-1", 2)]
// };

// Kết hợp với SetsRequiredMembers
public class Product
{
    public required string Name { get; init; }
    public required decimal Price { get; init; }

    [SetsRequiredMembers]
    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
}

4. Source Generators

Tại Sao Source Generators?

Reflection (runtime):
  Code → IL → JIT → Runtime inspect types → Slow, not AOT-friendly

Source Generators (compile-time):
  Code → Roslyn Analyzer → Generated C# code → IL → Fast, AOT-compatible

Các Source Generators Quan Trọng Trong .NET

csharp
// 1. System.Text.Json Source Generator
[JsonSerializable(typeof(List<Product>))]
[JsonSerializable(typeof(OrderResponse))]
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class AppJsonContext : JsonSerializerContext { }

// Sử dụng:
app.MapGet("/products", () =>
    TypedResults.Ok(products))
    .WithOpenApi();

// Trong Program.cs cho Minimal API:
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

// 2. Logging Source Generator (high-performance logging)
public static partial class LogMessages
{
    [LoggerMessage(Level = LogLevel.Information,
        Message = "Processing order {OrderId} for customer {CustomerId}")]
    public static partial void OrderProcessing(
        this ILogger logger, Guid orderId, string customerId);

    [LoggerMessage(Level = LogLevel.Error,
        Message = "Payment failed for order {OrderId}: {Reason}")]
    public static partial void PaymentFailed(
        this ILogger logger, Guid orderId, string reason, Exception ex);
}

// Sử dụng: logger.OrderProcessing(order.Id, order.CustomerId);
// → Zero-allocation logging, compile-time message validation

// 3. Regex Source Generator
public partial class Validators
{
    [GeneratedRegex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")]
    private static partial Regex EmailRegex();

    [GeneratedRegex(@"^\+?[1-9]\d{1,14}$")]
    private static partial Regex PhoneRegex();

    public static bool IsValidEmail(string email) => EmailRegex().IsMatch(email);
}
// → Compiled tại build time, ~5x nhanh hơn Regex.IsMatch()

Viết Custom Source Generator

csharp
// Ví dụ: Auto-generate interface từ class
[Generator]
public class AutoInterfaceGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 1. Tìm tất cả classes có [AutoInterface] attribute
        var classDeclarations = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "AutoInterfaceAttribute",
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, _) => GetClassInfo(ctx))
            .Where(x => x is not null);

        // 2. Generate interface source code
        context.RegisterSourceOutput(classDeclarations, (spc, classInfo) =>
        {
            var source = GenerateInterface(classInfo!);
            spc.AddSource($"I{classInfo!.Name}.g.cs", source);
        });
    }
}

5. Span<T>, Memory<T> & ref struct

Span<T> - Zero-Copy Slicing

csharp
// Span<T> là "view" vào memory, KHÔNG copy data
public static (ReadOnlySpan<char> scheme, ReadOnlySpan<char> host, ReadOnlySpan<char> path)
    ParseUrl(ReadOnlySpan<char> url)
{
    // "https://api.example.com/orders/123"
    int schemeEnd = url.IndexOf("://");
    var scheme = url[..schemeEnd];           // "https" - zero copy!

    var afterScheme = url[(schemeEnd + 3)..];
    int pathStart = afterScheme.IndexOf('/');
    var host = afterScheme[..pathStart];     // "api.example.com"
    var path = afterScheme[pathStart..];     // "/orders/123"

    return (scheme, host, path);
}

// So sánh với String: mỗi Substring() tạo string mới trên heap
// Span: zero allocation, chỉ là con trỏ + length

// stackalloc kết hợp Span
public static string FormatGuid(Guid id)
{
    Span<char> buffer = stackalloc char[36];
    id.TryFormat(buffer, out _);
    return new string(buffer);
}

Memory<T> - Async-Compatible Span

csharp
// Span<T> là ref struct → KHÔNG thể dùng trong async methods
// Memory<T> là wrapper cho phép truyền qua async boundary

public async Task ProcessDataAsync(Memory<byte> data)
{
    // Có thể truyền Memory<T> qua async
    await ProcessChunkAsync(data[..1024]);
    await ProcessChunkAsync(data[1024..]);
}

private async Task ProcessChunkAsync(Memory<byte> chunk)
{
    // Khi cần read, tạo Span từ Memory
    var span = chunk.Span;
    // Xử lý đồng bộ với span...
}

ref struct & Scoped

csharp
// ref struct: chỉ sống trên stack, KHÔNG thể escape lên heap
// Ví dụ: Span<T>, ReadOnlySpan<T>, Utf8JsonReader

ref struct ValueStringBuilder
{
    private Span<char> _buffer;
    private int _length;

    public ValueStringBuilder(Span<char> buffer)
    {
        _buffer = buffer;
        _length = 0;
    }

    public void Append(ReadOnlySpan<char> value)
    {
        value.CopyTo(_buffer[_length..]);
        _length += value.Length;
    }

    public override string ToString() => _buffer[.._length].ToString();
}

// Sử dụng: zero-allocation string building
Span<char> buffer = stackalloc char[256];
var sb = new ValueStringBuilder(buffer);
sb.Append("Hello ");
sb.Append("World");
var result = sb.ToString(); // Chỉ 1 allocation (final string)

6. Collection Expressions & Params Collections (C# 12/13)

csharp
// Collection Expressions (C# 12)
// Unified syntax cho mọi collection type
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
ImmutableArray<int> immutable = [1, 2, 3];

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

// Trong method return
public IReadOnlyList<string> GetDefaultRoles() => ["User", "Viewer"];

// Params collections (C# 13 / .NET 9)
// params hỗ trợ mọi collection type, không chỉ arrays
public void Log(params ReadOnlySpan<string> messages)
{
    foreach (var msg in messages)
        Console.WriteLine(msg);
}

// Gọi: Log("Hello", "World"); // zero allocation!
// Trước: params string[] → luôn allocate array

// Real-world: builder pattern
public QueryBuilder Where(params ReadOnlySpan<string> conditions)
{
    foreach (var condition in conditions)
        _conditions.Add(condition);
    return this;
}

var query = new QueryBuilder()
    .Where("status = 'active'", "price > 100", "category = 'electronics'");

7. Interceptors (C# 12 Preview)

csharp
// Interceptors cho phép Source Generators "thay thế" method calls
// tại compile time → tối ưu performance

// Original code:
app.MapGet("/products", () => db.Products.ToListAsync());

// Source Generator có thể intercept và thay bằng optimized version:
// [InterceptsLocation("Program.cs", line: 5, column: 5)]
// public static RouteHandlerBuilder MapGet_Optimized(...)

// ⚠️ Đây là feature experimental, chủ yếu dùng bởi framework authors
// (ASP.NET Core dùng để optimize Minimal API route handlers)

8. Tổng Kết & Best Practices

FeatureKhi nào dùngKhi nào KHÔNG dùng
Pattern MatchingComplex conditionals, type checksĐơn giản if/else đủ
RecordsDTOs, Events, Value ObjectsEntities cần mutable state
Primary ConstructorsDI injection, simple constructorsCần complex init logic
Source GeneratorsHigh-perf serialization, loggingSimple one-off tasks
Span<T>Hot paths, parsing, zero-copyAsync methods (dùng Memory<T>)
Collection ExpressionsMọi nơi tạo collection literal-

Bài tập: Refactor 1 service class trong project của bạn: dùng Primary Constructor cho DI, [LoggerMessage] cho logging, và pattern matching cho business logic. Đo performance trước/sau bằng BenchmarkDotNet.