- C# 14 Mastery Series Part 1: Introduction to C# 14 – What’s New and Why It Matters
- C# 14 Mastery Series Part 2: Field-Backed Properties Deep Dive – The `field` Keyword Revolution
- C# 14 Mastery Series Part 3: Extension Members Foundation – New Syntax and Capabilities
- C# 14 Mastery Series Part 5: Generic Types Enhancement – nameof with Unbound Generics
- C# 14 Mastery Series Part 4: Advanced Extension Members – Properties and Complex Scenarios
- C# 14 Mastery Series Part 6: Performance Analysis – Benchmarking C# 14 Features
- C# 14 Mastery Series Part 7: Migration Strategies – From C# 13 to C# 14
- C# 14 Mastery Series Part 8: Real-World Implementation – Building Production Applications
Welcome to the final part of our C# 14 mastery series! After seven comprehensive parts covering features, performance, and migration strategies, it’s time to bring everything together. Today we’ll build complete, production-ready applications that demonstrate the full power of C# 14, showcase advanced architectural patterns, and provide real-world implementation examples you can apply immediately.
Complete E-Commerce Application
Domain Models with Field-Backed Properties
public class Product
{
public int Id { get; set; }
// Field-backed properties with validation and transformation
public string Name
{
get => field;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Product name cannot be empty")
: value.Trim();
}
public decimal Price
{
get => field;
set => field = value >= 0
? Math.Round(value, 2, MidpointRounding.AwayFromZero)
: throw new ArgumentException("Price cannot be negative");
}
public int StockQuantity
{
get => field;
set
{
if (value < 0) throw new ArgumentException("Stock cannot be negative");
var oldValue = field;
field = value;
if (oldValue > 0 && value == 0)
OnStockDepleted();
}
}
public string Slug
{
get => field ??= GenerateSlug(Name);
set => field = value;
}
public event EventHandler StockDepleted;
private static string GenerateSlug(string name) =>
name?.ToLowerInvariant().Replace(" ", "-") ?? string.Empty;
private void OnStockDepleted() => StockDepleted?.Invoke(this, EventArgs.Empty);
}
Extension Properties for Business Logic
extension ProductExtensions for Product
{
// Business logic properties
public bool IsAvailable => this.StockQuantity > 0 && this.IsActive;
public bool IsLowStock => this.StockQuantity > 0 && this.StockQuantity <= 5;
public bool IsOnSale => this.SalePrice.HasValue && this.SalePrice < this.Price;
// Display properties
public string DisplayPrice => this.IsOnSale
? $"${this.SalePrice:F2} (was ${this.Price:F2})"
: $"${this.Price:F2}";
public string StockStatus => this.StockQuantity switch
{
0 => "Out of Stock",
var qty when qty <= 5 => $"Low Stock ({qty} remaining)",
_ => "In Stock"
};
// Pricing calculations
public decimal EffectivePrice => this.SalePrice ?? this.Price;
public decimal? SavingsAmount => this.IsOnSale ? this.Price - this.SalePrice : null;
// Static factory methods
public static Product CreateSample(string name, decimal price) => new()
{
Name = name,
Price = price,
StockQuantity = 100,
IsActive = true
};
}
Advanced Repository Pattern
Generic Repository with Extensions
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task<T> UpdateAsync(T entity);
Task DeleteAsync(int id);
}
extension RepositoryExtensions<T> for IRepository<T> where T : class
{
// Query capabilities
public bool HasData => this.GetAllAsync().Result.Any();
public bool IsEmpty => !this.HasData;
// Batch operations
public async Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entities)
{
var results = new List<T>();
foreach (var entity in entities)
{
results.Add(await this.AddAsync(entity));
}
return results;
}
// Caching capabilities
private static readonly ConcurrentDictionary<string, object> _cache = new();
public async Task<T> GetByIdCachedAsync(int id, TimeSpan? expiration = null)
{
var cacheKey = $"{nameof(T)}_{id}";
var expirationTime = expiration ?? TimeSpan.FromMinutes(5);
if (_cache.TryGetValue(cacheKey, out var cached) &&
cached is CachedItem<T> item &&
item.ExpiresAt > DateTime.UtcNow)
{
return item.Value;
}
var entity = await this.GetByIdAsync(id);
if (entity != null)
{
_cache[cacheKey] = new CachedItem<T>
{
Value = entity,
ExpiresAt = DateTime.UtcNow.Add(expirationTime)
};
}
return entity;
}
// Static configuration
public static int DefaultPageSize { get; set; } = 20;
public static int MaxPageSize { get; set; } = 100;
// Pagination
public async Task<PagedResult<T>> GetPagedAsync(int page = 1, int pageSize = 0)
{
pageSize = pageSize <= 0 ? DefaultPageSize : Math.Min(pageSize, MaxPageSize);
var allItems = await this.GetAllAsync();
var items = allItems.Skip((page - 1) * pageSize).Take(pageSize);
var totalCount = allItems.Count();
return new PagedResult<T>
{
Items = items.ToList(),
CurrentPage = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling((double)totalCount / pageSize)
};
}
}
Modern API Controllers
RESTful API with C# 14
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IRepository<Product> _repository;
private readonly ILogger<ProductsController> _logger;
public ProductsController(IRepository<Product> repository, ILogger<ProductsController> logger)
{
_repository = repository;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<ApiResponse<IEnumerable<ProductViewModel>>>> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
try
{
var pagedResult = await _repository.GetPagedAsync(page, pageSize);
var viewModels = pagedResult.Items.Select(p => p.ToViewModel());
return Ok(ApiResponse<IEnumerable<ProductViewModel>>.Success(
viewModels,
$"Retrieved {pagedResult.Items.Count} of {pagedResult.TotalCount} products"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving products");
return StatusCode(500, ApiResponse<IEnumerable<ProductViewModel>>.Error("An error occurred"));
}
}
[HttpGet("{id:int}")]
public async Task<ActionResult<ApiResponse<ProductDetailViewModel>>> GetProduct(int id)
{
var product = await _repository.GetByIdCachedAsync(id);
if (product == null)
return NotFound(ApiResponse<ProductDetailViewModel>.Error("Product not found"));
var viewModel = product.ToDetailViewModel();
return Ok(ApiResponse<ProductDetailViewModel>.Success(viewModel));
}
[HttpPost]
public async Task<ActionResult<ApiResponse<ProductViewModel>>> CreateProduct([FromBody] CreateProductRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ApiResponse<ProductViewModel>.Error("Invalid product data"));
try
{
var product = request.ToProduct();
var created = await _repository.AddAsync(product);
var viewModel = created.ToViewModel();
return CreatedAtAction(nameof(GetProduct), new { id = created.Id },
ApiResponse<ProductViewModel>.Success(viewModel));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating product");
return StatusCode(500, ApiResponse<ProductViewModel>.Error("An error occurred"));
}
}
}
// Mapping extensions
extension ProductMappingExtensions for Product
{
public ProductViewModel ToViewModel() => new()
{
Id = this.Id,
Name = this.Name,
Price = this.DisplayPrice,
IsAvailable = this.IsAvailable,
StockStatus = this.StockStatus
};
public ProductDetailViewModel ToDetailViewModel() => new()
{
Id = this.Id,
Name = this.Name,
Description = this.Description,
Price = this.DisplayPrice,
OriginalPrice = this.IsOnSale ? this.Price : null,
IsAvailable = this.IsAvailable,
StockStatus = this.StockStatus
};
}
extension CreateProductRequestExtensions for CreateProductRequest
{
public Product ToProduct() => new()
{
Name = this.Name,
Description = this.Description,
Price = this.Price,
StockQuantity = this.StockQuantity,
IsActive = true,
CreatedAt = DateTime.UtcNow
};
}
Configuration with Field-Backed Properties
public class DatabaseOptions
{
public string ConnectionString
{
get => field;
set => field = string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Connection string cannot be empty")
: value;
}
public TimeSpan CommandTimeout
{
get => field == TimeSpan.Zero ? TimeSpan.FromSeconds(30) : field;
set => field = value > TimeSpan.Zero
? value
: throw new ArgumentException("Command timeout must be positive");
}
public int MaxRetryAttempts
{
get => field == 0 ? 3 : field;
set => field = Math.Max(0, Math.Min(value, 10));
}
}
public class CacheOptions
{
public TimeSpan DefaultExpiration
{
get => field == TimeSpan.Zero ? TimeSpan.FromMinutes(5) : field;
set => field = value;
}
public int MaxCacheSize
{
get => field == 0 ? 1000 : field;
set => field = Math.Max(0, value);
}
public bool EnableDistributedCache
{
get => field;
set => field = value;
}
}
Dependency Injection Setup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Repository registration using nameof for type-safe logging
services.AddRepositories(typeof(Product), typeof(Order), typeof(Customer));
// Configure options
services.Configure<DatabaseOptions>(Configuration.GetSection("Database"));
services.Configure<CacheOptions>(Configuration.GetSection("Cache"));
// Add application services
services.AddApplicationServices();
// Add API services
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
}
}
extension ServiceCollectionExtensions for IServiceCollection
{
public IServiceCollection AddRepositories(params Type[] entityTypes)
{
foreach (var entityType in entityTypes)
{
var repositoryInterface = typeof(IRepository<>).MakeGenericType(entityType);
var repositoryImplementation = typeof(Repository<>).MakeGenericType(entityType);
this.AddScoped(repositoryInterface, repositoryImplementation);
}
return this;
}
public IServiceCollection AddApplicationServices()
{
this.AddScoped<ProductService>();
this.AddScoped<OrderService>();
this.AddScoped<CustomerService>();
this.AddScoped<ICacheService, CacheService>();
return this;
}
// Type-safe service registration using nameof
public IServiceCollection AddTypedService<TInterface, TImplementation>()
where TImplementation : class, TInterface
where TInterface : class
{
this.AddScoped<TInterface, TImplementation>();
var logger = this.BuildServiceProvider().GetService<ILogger<Startup>>();
logger?.LogInformation("Registered {Interface} with {Implementation}",
nameof(TInterface), nameof(TImplementation));
return this;
}
}
Testing with C# 14 Features
public class ProductTests
{
[Fact]
public void Product_Name_ShouldTrimWhitespace()
{
// Arrange
var product = new Product();
// Act
product.Name = " Test Product ";
// Assert
Assert.Equal("Test Product", product.Name);
}
[Fact]
public void Product_Price_ShouldRoundToTwoDecimals()
{
// Arrange
var product = new Product();
// Act
product.Price = 19.999m;
// Assert
Assert.Equal(20.00m, product.Price);
}
[Fact]
public void Product_IsAvailable_ShouldReturnTrue_WhenInStock()
{
// Arrange
var product = new Product
{
StockQuantity = 10,
IsActive = true
};
// Act & Assert
Assert.True(product.IsAvailable);
}
[Fact]
public void Repository_GetPagedAsync_ShouldReturnCorrectPage()
{
// Arrange
var repository = new Mock<IRepository<Product>>();
var products = GenerateTestProducts(50);
repository.Setup(r => r.GetAllAsync()).ReturnsAsync(products);
// Act
var result = repository.Object.GetPagedAsync(2, 10).Result;
// Assert
Assert.Equal(2, result.CurrentPage);
Assert.Equal(10, result.PageSize);
Assert.Equal(50, result.TotalCount);
Assert.Equal(5, result.TotalPages);
Assert.Equal(10, result.Items.Count);
}
[Theory]
[InlineData("laptop", 1299.99, true)]
[InlineData("", 100.00, false)] // Empty name should throw
[InlineData("mouse", -50.00, false)] // Negative price should throw
public void Product_Creation_ShouldValidateInputs(string name, decimal price, bool shouldSucceed)
{
// Act & Assert
if (shouldSucceed)
{
var product = new Product { Name = name, Price = price };
Assert.Equal(name, product.Name);
Assert.Equal(price, product.Price);
}
else
{
Assert.ThrowsAny<ArgumentException>(() =>
{
var product = new Product { Name = name, Price = price };
});
}
}
private static List<Product> GenerateTestProducts(int count)
{
return Enumerable.Range(1, count)
.Select(i => Product.CreateSample($"Product {i}", i * 10))
.ToList();
}
}
Production Deployment Considerations
Performance Monitoring
public class PerformanceMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<PerformanceMiddleware> _logger;
public PerformanceMiddleware(RequestDelegate next, ILogger<PerformanceMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
try
{
await _next(context);
}
finally
{
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 1000) // Log slow requests
{
_logger.LogWarning("Slow request detected: {Method} {Path} took {ElapsedMs}ms",
context.Request.Method,
context.Request.Path,
stopwatch.ElapsedMilliseconds);
}
}
}
}
// Extension properties for monitoring
extension HttpContextExtensions for HttpContext
{
public bool IsApiRequest => this.Request.Path.StartsWithSegments("/api");
public bool IsHealthCheck => this.Request.Path.StartsWithSegments("/health");
public string RequestIdentifier => $"{this.Request.Method} {this.Request.Path}";
// Performance tracking
public void TrackPerformance(string operationName, TimeSpan duration)
{
this.Items[$"Performance_{operationName}"] = duration;
}
public TimeSpan? GetPerformance(string operationName)
{
return this.Items.TryGetValue($"Performance_{operationName}", out var value)
? value as TimeSpan?
: null;
}
}
Key Takeaways and Best Practices
From building this complete application with C# 14, here are the essential takeaways:
Field-Backed Properties Best Practices
- Validation in Setters: Perform validation and transformation when data is set, not when read
- Lazy Loading: Use null-coalescing assignment for expensive computations
- Event Triggers: Integrate business events directly into property setters
- Performance: Avoid expensive operations in getters
Extension Members Best Practices
- Logical Grouping: Organize related functionality in single extension blocks
- Static Utilities: Include helper methods and factory functions
- Computed Properties: Create intuitive, business-focused property names
- Caching: Use static caching for expensive computations
Generic Type Enhancements
- Type Safety: Use nameof with unbound generics for refactoring safety
- Logging: Eliminate hardcoded type names in logs and error messages
- Configuration: Create type-safe configuration registration
- Performance: Zero runtime overhead compared to hardcoded strings
Conclusion
C# 14 represents a significant evolution in the language, providing developers with powerful tools to write more maintainable, performant, and expressive code. Through this comprehensive series, we've explored how these features work individually and, more importantly, how they work together to solve real-world problems.
The complete e-commerce application we built demonstrates that C# 14 features aren't just syntactic sugar—they enable better software architecture, cleaner code organization, and improved developer productivity. Field-backed properties reduce boilerplate while adding functionality, extension members create more intuitive APIs, and enhanced generic support provides better type safety.
As you adopt C# 14 in your projects, remember that the best implementations combine multiple features thoughtfully. Start small, measure the impact, and gradually expand your usage as your team becomes comfortable with the new capabilities.
The future of C# development is exciting, and C# 14 provides a solid foundation for building the next generation of applications. Happy coding!
Thank you for following along with the C# 14 Mastery Series!