
📖 Table of Contents
Welcome to C# 12: The Modern C# Revolution
Primary Constructors: Eliminating Boilerplate Code
Records: Immutable Data Made Simple
Advanced Pattern Matching: Smart Code Flow
Collection Expressions: Simplified Collections
Inline Arrays: High-Performance Scenarios
Default Interface Methods: Evolving APIs
Alias Any Type: Cleaner Code Organization
Global Using Directives: Reduced Clutter
Interpolated String Handlers: Performance & Control
Real-World ASP.NET Core Integration
Performance Benchmarks & Optimization
Migration Strategies from Older Versions
Best Practices & Common Pitfalls
What's Next: Advanced C# Features in Part 5
1. Welcome to C# 12: The Modern C# Revolution 🚀
1.1 The C# Evolution: From 1.0 to 12.0
Welcome to the most exciting evolution in C# history! C# 12 represents a fundamental shift towards simplicity, performance, and expressiveness. If you're still writing C# like it's 2010, you're working too hard.
Historical Perspective :
C# 1.0 (2002): Basic OOP, similar to Java
C# 3.0 (2007): LINQ, lambda expressions, var keyword
C# 5.0 (2012): Async/await revolution
C# 8.0 (2019): Nullable reference types, patterns
C# 12.0 (2023): Primary constructors, records, modern patterns
1.2 Why C# 12 Matters for ASP.NET Core Developers
csharp
// Before C# 12 - Traditional ASP.NET Core Controller
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductsController> _logger;
private readonly IMapper _mapper;
public ProductsController(
IProductRepository repository,
ILogger<ProductsController> logger,
IMapper mapper)
{
_repository = repository;
_logger = logger;
_mapper = mapper;
}
// 10+ lines of boilerplate constructor code 😞
}
// After C# 12 - Modern ASP.NET Core Controller
public class ProductsController(
IProductRepository repository,
ILogger<ProductsController> logger,
IMapper mapper) : ControllerBase
{
// Zero boilerplate! All dependencies available directly 🎉
// The constructor is generated automatically
}
Real Impact : C# 12 can reduce your codebase by 20-40% while making it more readable and maintainable.
2. Primary Constructors: Eliminating Boilerplate Code 🏗️
2.1 Understanding Primary Constructors
Primary constructors are arguably the most significant feature in C# 12. They eliminate the ceremony of traditional constructor writing.
csharp
// 🚨 BEFORE C# 12: Traditional Class with Constructor
public class ProductService
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
private readonly ICacheService _cache;
private readonly IMapper _mapper;
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger,
ICacheService cache,
IMapper mapper)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
// 15+ lines of boilerplate just for constructor! 😫
}
// ✅ AFTER C# 12: Primary Constructor Magic
public class ProductService(
IProductRepository repository,
ILogger<ProductService> logger,
ICacheService cache,
IMapper mapper)
{
// All parameters are automatically available as private fields!
// No boilerplate, no ceremony, just clean code 🎉
public async Task<ProductDto> GetProductAsync(int id)
{
logger.LogInformation("Fetching product {ProductId}", id);
var product = await repository.GetByIdAsync(id);
return mapper.Map<ProductDto>(product);
}
}
2.2 Real-World ASP.NET Core Integration
csharp
// Modern ASP.NET Core Controllers with Primary Constructors
public class OrdersController(
IOrderService orderService,
ILogger<OrdersController> logger,
IEmailService emailService,
IPaymentGateway paymentGateway) : ControllerBase
{
[HttpGet("{id}")]
public async Task<ActionResult<OrderDto>> GetOrder(int id)
{
logger.LogInformation("Retrieving order {OrderId}", id);
var order = await orderService.GetOrderAsync(id);
if (order == null)
return NotFound();
return Ok(order);
}
[HttpPost]
public async Task<ActionResult<OrderDto>> CreateOrder(CreateOrderRequest request)
{
logger.LogInformation("Creating new order for customer {CustomerId}", request.CustomerId);
var order = await orderService.CreateOrderAsync(request);
await emailService.SendOrderConfirmationAsync(order);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
}
// Service Layer with Primary Constructors
public class OrderService(
IOrderRepository orderRepository,
IProductRepository productRepository,
IShippingService shippingService,
ILogger<OrderService> logger)
{
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
logger.LogInformation("Creating order with {ItemCount} items", request.Items.Count);
// Validate products and stock
foreach (var item in request.Items)
{
var product = await productRepository.GetByIdAsync(item.ProductId);
if (product == null)
throw new ArgumentException($"Product {item.ProductId} not found");
if (product.StockQuantity < item.Quantity)
throw new InvalidOperationException($"Insufficient stock for product {product.Name}");
}
var order = new Order
{
CustomerId = request.CustomerId,
Items = request.Items,
Status = OrderStatus.Pending
};
await orderRepository.AddAsync(order);
await shippingService.ScheduleShippingAsync(order);
return order;
}
}
2.3 Advanced Primary Constructor Scenarios
csharp
// Primary Constructors with Validation
public class ValidatedProductService(
IProductRepository repository,
ILogger<ValidatedProductService> logger,
ICacheService cache)
{
// Parameter validation in initializers
private readonly IProductRepository _repository = repository ??
throw new ArgumentNullException(nameof(repository));
private readonly ILogger<ValidatedProductService> _logger = logger ??
throw new ArgumentNullException(nameof(logger));
private readonly ICacheService _cache = cache ??
throw new ArgumentNullException(nameof(cache));
public async Task<Product> GetProductAsync(int id)
{
_logger.LogInformation("Fetching product {ProductId}", id);
return await _repository.GetByIdAsync(id);
}
}
// Primary Constructors in Base Classes
public abstract class BaseService(
ILogger logger,
ICacheService cache)
{
protected ILogger Logger { get; } = logger;
protected ICacheService Cache { get; } = cache;
protected async Task<T> CacheGetOrCreateAsync<T>(string key, Func<Task<T>> factory)
{
var cached = await Cache.GetAsync<T>(key);
if (cached != null)
return cached;
var result = await factory();
await Cache.SetAsync(key, result, TimeSpan.FromMinutes(30));
return result;
}
}
public class ProductService(
IProductRepository repository,
ILogger<ProductService> logger,
ICacheService cache)
: BaseService(logger, cache) // Passing parameters to base class
{
public async Task<Product> GetProductAsync(int id)
{
var cacheKey = $"product_{id}";
return await CacheGetOrCreateAsync(cacheKey,
() => repository.GetByIdAsync(id));
}
}
3. Records: Immutable Data Made Simple 📝
3.1 Understanding Records for DTOs and Models
Records provide a concise way to create immutable reference types with value-based equality. Perfect for DTOs, API models, and immutable data structures.
csharp
// 🚨 BEFORE: Traditional DTO Classes
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public int StockQuantity { get; set; }
// Need to manually implement equality, ToString(), etc.
public override bool Equals(object? obj)
{
return obj is ProductDto dto &&
Id == dto.Id &&
Name == dto.Name &&
Price == dto.Price &&
Category == dto.Category &&
StockQuantity == dto.StockQuantity;
}
public override int GetHashCode()
{
return HashCode.Combine(Id, Name, Price, Category, StockQuantity);
}
// 20+ lines of boilerplate! 😫
}
// ✅ AFTER: Records for Concise DTOs
public record ProductDto(
int Id,
string Name,
decimal Price,
string Category,
int StockQuantity);
// That's it! 1 line instead of 20+ 🎉
// Automatic value-based equality
// Automatic ToString() implementation
// Automatic deconstruction support
// Immutable by default
3.2 Real-World ASP.NET Core API Examples
csharp
// API Request/Response Records
public record CreateProductRequest(
string Name,
string Description,
decimal Price,
string Category,
int StockQuantity,
string? ImageUrl = null);
public record ProductResponse(
int Id,
string Name,
string Description,
decimal Price,
string Category,
int StockQuantity,
string ImageUrl,
DateTime CreatedAt,
bool IsInStock);
public record PagedResponse<T>(
IReadOnlyList<T> Items,
int PageNumber,
int PageSize,
int TotalCount,
int TotalPages)
{
public bool HasPreviousPage => PageNumber > 1;
public bool HasNextPage => PageNumber < TotalPages;
}
// Modern ASP.NET Core Controller using Records
[ApiController]
[Route("api/[controller]")]
public class ProductsController(
IProductService productService,
ILogger<ProductsController> logger) : ControllerBase
{
[HttpGet]
public async Task<ActionResult<PagedResponse<ProductResponse>>> GetProducts(
[FromQuery] ProductQuery query)
{
logger.LogInformation("Fetching products with query {@Query}", query);
var (products, totalCount) = await productService.GetProductsAsync(query);
var response = new PagedResponse<ProductResponse>(
products,
query.PageNumber,
query.PageSize,
totalCount,
(int)Math.Ceiling(totalCount / (double)query.PageSize));
return Ok(response);
}
[HttpPost]
public async Task<ActionResult<ProductResponse>> CreateProduct(CreateProductRequest request)
{
logger.LogInformation("Creating new product: {ProductName}", request.Name);
var product = await productService.CreateProductAsync(request);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpPut("{id}")]
public async Task<ActionResult<ProductResponse>> UpdateProduct(
int id,
UpdateProductRequest request)
{
logger.LogInformation("Updating product {ProductId}", id);
var product = await productService.UpdateProductAsync(id, request);
return Ok(product);
}
}
// Query Records for API Parameters
public record ProductQuery(
string? SearchTerm = null,
string? Category = null,
decimal? MinPrice = null,
decimal? MaxPrice = null,
string? SortBy = "name",
bool SortDescending = false,
int PageNumber = 1,
int PageSize = 20)
{
public bool HasSearch => !string.IsNullOrWhiteSpace(SearchTerm);
public bool HasCategoryFilter => !string.IsNullOrWhiteSpace(Category);
public bool HasPriceFilter => MinPrice.HasValue || MaxPrice.HasValue;
}
3.3 Advanced Record Features
csharp
// Records with Methods and Custom Logic
public record Product(
int Id,
string Name,
string Description,
decimal Price,
string Category,
int StockQuantity,
DateTime CreatedAt)
{
// Computed properties
public bool IsInStock => StockQuantity > 0;
public bool IsExpensive => Price > 1000;
public string PriceCategory => Price switch
{
< 50 => "Budget",
< 200 => "Mid-range",
_ => "Premium"
};
// Methods
public Product WithDiscount(decimal discountPercentage)
{
if (discountPercentage < 0 || discountPercentage > 100)
throw new ArgumentException("Discount must be between 0 and 100");
var discountedPrice = Price * (1 - discountPercentage / 100);
return this with { Price = discountedPrice };
}
public Product ReduceStock(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive");
if (quantity > StockQuantity)
throw new InvalidOperationException("Insufficient stock");
return this with { StockQuantity = StockQuantity - quantity };
}
}
// Record Structs for Performance
public readonly record struct Point3D(double X, double Y, double Z)
{
public double Magnitude => Math.Sqrt(X * X + Y * Y + Z * Z);
public static Point3D Origin => new(0, 0, 0);
public Point3D Normalize()
{
var mag = Magnitude;
return mag == 0 ? Origin : new Point3D(X / mag, Y / mag, Z / mag);
}
}
// Using Records in ASP.NET Core Services
public class ProductService(
IProductRepository repository,
ILogger<ProductService> logger)
{
public async Task<Product> CreateProductAsync(CreateProductRequest request)
{
logger.LogInformation("Creating product: {ProductName}", request.Name);
var product = new Product(
Id: 0, // Will be set by database
Name: request.Name,
Description: request.Description,
Price: request.Price,
Category: request.Category,
StockQuantity: request.StockQuantity,
CreatedAt: DateTime.UtcNow);
return await repository.AddAsync(product);
}
public async Task<Product> ApplyDiscountAsync(int productId, decimal discountPercentage)
{
var product = await repository.GetByIdAsync(productId);
if (product == null)
throw new ArgumentException($"Product {productId} not found");
var discountedProduct = product.WithDiscount(discountPercentage);
return await repository.UpdateAsync(discountedProduct);
}
}
4. Advanced Pattern Matching: Smart Code Flow 🧩
4.1 Comprehensive Pattern Matching Guide
Pattern matching transforms how we write conditional logic, making it more expressive and less error-prone.
csharp
// 🚨 BEFORE: Traditional Conditional Logic
public decimal CalculateShippingCost(object order)
{
if (order is DomesticOrder domestic)
{
if (domestic.Weight > 10)
return 15.99m;
else if (domestic.Weight > 5)
return 9.99m;
else
return 4.99m;
}
else if (order is InternationalOrder international)
{
if (international.Destination == "EU")
return 29.99m;
else if (international.Destination == "Asia")
return 39.99m;
else
return 49.99m;
}
else if (order is ExpressOrder express)
{
return express.Weight * 2.5m;
}
else
{
throw new ArgumentException("Unknown order type");
}
}
// ✅ AFTER: Modern Pattern Matching
public decimal CalculateShippingCost(object order) => order switch
{
DomesticOrder { Weight: > 10 } => 15.99m,
DomesticOrder { Weight: > 5 } => 9.99m,
DomesticOrder => 4.99m,
InternationalOrder { Destination: "EU" } => 29.99m,
InternationalOrder { Destination: "Asia" } => 39.99m,
InternationalOrder => 49.99m,
ExpressOrder express => express.Weight * 2.5m,
_ => throw new ArgumentException("Unknown order type")
};
4.2 Real-World ASP.NET Core Application
csharp
// Pattern Matching in API Request Handling
public class OrderProcessingService(
IOrderRepository orderRepository,
IPaymentService paymentService,
IShippingService shippingService,
ILogger<OrderProcessingService> logger)
{
public async Task<OrderResult> ProcessOrderAsync(OrderRequest request)
{
logger.LogInformation("Processing order {OrderId}", request.OrderId);
var result = request switch
{
// Online payment with credit card
{ PaymentMethod: PaymentMethod.CreditCard, Amount: > 0 } order
=> await ProcessCreditCardOrderAsync(order),
// PayPal payment
{ PaymentMethod: PaymentMethod.PayPal } order
=> await ProcessPayPalOrderAsync(order),
// Bank transfer for large amounts
{ PaymentMethod: PaymentMethod.BankTransfer, Amount: >= 1000 } order
=> await ProcessBankTransferOrderAsync(order),
// Invalid cases
{ Amount: <= 0 } => OrderResult.Failed("Invalid order amount"),
null => OrderResult.Failed("Order request is null"),
// Default case
_ => OrderResult.Failed("Unsupported payment method")
};
return result;
}
public async Task<OrderValidationResult> ValidateOrderAsync(Order order)
{
return order switch
{
// Valid domestic order
{ ShippingAddress.Country: "US", Items.Count: > 0 }
=> OrderValidationResult.Valid(),
// International order with restrictions
{ ShippingAddress.Country: not "US", Items: var items }
when items.Any(i => i.IsRestricted)
=> OrderValidationResult.Failed("Contains restricted items for international shipping"),
// Empty order
{ Items.Count: 0 }
=> OrderValidationResult.Failed("Order must contain at least one item"),
// Large order requiring verification
{ TotalAmount: > 5000 }
=> OrderValidationResult.RequiresVerification(),
// Default valid case
_ => OrderValidationResult.Valid()
};
}
}
// Advanced Pattern Matching with Property Patterns
public class NotificationService
{
public string GenerateNotificationMessage(object eventData) => eventData switch
{
// Order shipped notification
OrderShippedEvent { Order: var order, TrackingNumber: not null }
=> $"Your order #{order.Id} has been shipped. Tracking: {order.TrackingNumber}",
// Payment failed notification
PaymentFailedEvent { OrderId: var orderId, Reason: var reason }
=> $"Payment failed for order #{orderId}. Reason: {reason}",
// Low stock alert
LowStockEvent { Product: { Name: var name, StockQuantity: < 5 } }
=> $"Low stock alert: {name} has only {product.StockQuantity} items left",
// New user welcome
UserRegisteredEvent { User: { Email: var email, IsPremium: true } }
=> $"Welcome premium user {email}! Enjoy exclusive benefits.",
UserRegisteredEvent { User: { Email: var email } }
=> $"Welcome {email}! Start exploring our products.",
// Default case
_ => "Notification: You have an update."
};
}
// List Patterns for Collection Processing
public class AnalyticsService
{
public string AnalyzeSalesTrend(decimal[] dailySales) => dailySales switch
{
// Consistent growth
[_, .., var last] when last > dailySales[0] * 1.5m
=> "Strong growth trend",
// Weekend spike pattern
[.., var sat, var sun] when sun > sat * 1.2m
=> "Weekend sales spike",
// Seasonal pattern (last 3 days increasing)
[.., var d3, var d2, var d1] when d1 > d2 && d2 > d3
=> "Recent upward trend",
// Empty or single day
[] or [_]
=> "Insufficient data",
// Default case
_ => "Stable sales pattern"
};
public bool IsPeakSeason(DateTime date) => date switch
{
// Black Friday (4th Thursday of November + 1 day)
{ Month: 11, Day: var day }
when IsBlackFriday(date.Year, 11, day) => true,
// Christmas season
{ Month: 12, Day: >= 15 and <= 31 } => true,
// Summer sales (June-July)
{ Month: >= 6 and <= 7 } => true,
// Regular season
_ => false
};
private static bool IsBlackFriday(int year, int month, int day)
{
// Simplified Black Friday calculation
var thanksgiving = new DateTime(year, month, 1)
.AddDays((14 - (int)new DateTime(year, month, 1).DayOfWeek) % 7)
.AddDays(21);
return day == thanksgiving.AddDays(1).Day;
}
}

Join the conversation! Your thoughts help the community grow.