7.2 - Advanced Language Features
C# has evolved significantly since its initial release, introducing powerful features that enhance developer productivity, code expressiveness, and application performance. This chapter explores advanced language features that distinguish C# as a modern, versatile programming language.
7.2.1 - Delegates and Events
Delegates provide a type-safe, object-oriented way to reference methods, enabling scenarios like callbacks, event handling, and lambda expressions.
7.2.1.1 - Delegate Basics
A delegate is a type that represents references to methods with a specific parameter list and return type:
// Delegate declaration
public delegate int Calculator(int x, int y);
// Methods that match the delegate signature
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
// Using delegates
public static void DelegateExample()
{
// Create delegate instances
Calculator calc1 = Add;
Calculator calc2 = Multiply;
// Invoke delegates
Console.WriteLine($"Addition: {calc1(5, 3)}"); // Output: 8
Console.WriteLine($"Multiplication: {calc2(5, 3)}"); // Output: 15
// Multicast delegate
Calculator multiCalc = calc1 + calc2;
int result = multiCalc(5, 3); // Only returns the last result (15)
// Delegate as a parameter
ProcessNumbers(10, 20, Add);
}
public static void ProcessNumbers(int x, int y, Calculator operation)
{
int result = operation(x, y);
Console.WriteLine($"Result: {result}");
}
7.2.1.2 - Built-in Delegate Types
C# provides built-in generic delegate types for common scenarios:
// Action delegates (for methods that return void)
Action<string> displayMessage = message => Console.WriteLine(message);
displayMessage("Hello, World!");
// Func delegates (for methods that return a value)
Func<int, int, int> add = (a, b) => a + b;
int sum = add(5, 3); // 8
// Predicate delegate (for methods that return bool)
Predicate<int> isEven = number => number % 2 == 0;
bool result = isEven(4); // true
7.2.1.3 - Events
Events provide a way to implement the publisher-subscriber pattern, allowing objects to notify other objects when something of interest occurs:
public class Publisher
{
// Define the event using a built-in delegate type
public event EventHandler<string> MessagePublished;
// Method to raise the event
public void PublishMessage(string message)
{
// Check if there are any subscribers
MessagePublished?.Invoke(this, message);
}
}
public class Subscriber
{
public void Subscribe(Publisher publisher)
{
// Subscribe to the event
publisher.MessagePublished += OnMessagePublished;
}
public void Unsubscribe(Publisher publisher)
{
// Unsubscribe from the event
publisher.MessagePublished -= OnMessagePublished;
}
private void OnMessagePublished(object sender, string message)
{
Console.WriteLine($"Received message: {message}");
}
}
// Usage
public static void EventExample()
{
var publisher = new Publisher();
var subscriber = new Subscriber();
subscriber.Subscribe(publisher);
publisher.PublishMessage("Hello, Events!"); // Output: Received message: Hello, Events!
subscriber.Unsubscribe(publisher);
publisher.PublishMessage("This won't be received"); // No output
}
7.2.1.4 - Custom Event Arguments
For more complex event data, you can create custom event argument classes:
public class OrderEventArgs : EventArgs
{
public string OrderId { get; }
public decimal Amount { get; }
public DateTime OrderDate { get; }
public OrderEventArgs(string orderId, decimal amount, DateTime orderDate)
{
OrderId = orderId;
Amount = amount;
OrderDate = orderDate;
}
}
public class OrderProcessor
{
public event EventHandler<OrderEventArgs> OrderProcessed;
public void ProcessOrder(string orderId, decimal amount)
{
// Process the order...
// Notify subscribers
OrderProcessed?.Invoke(this, new OrderEventArgs(orderId, amount, DateTime.Now));
}
}
7.2.2 - Lambda Expressions
Lambda expressions provide a concise way to create anonymous methods or function objects, often used with delegates, LINQ, and asynchronous programming.
7.2.2.1 - Basic Syntax
// Expression lambda (implicit return)
Func<int, int> square = x => x * x;
// Statement lambda (explicit return and multiple statements)
Func<int, int> factorial = n =>
{
int result = 1;
for (int i = 1; i <= n; i++)
{
result *= i;
}
return result;
};
// Multiple parameters
Func<int, int, bool> isBetween = (value, limit) => value > 0 && value < limit;
// No parameters
Action sayHello = () => Console.WriteLine("Hello!");
7.2.2.2 - Capturing Variables
Lambda expressions can capture variables from the enclosing scope (closures):
public static List<Func<int>> CreateCounters(int numCounters)
{
var counters = new List<Func<int>>();
for (int i = 0; i < numCounters; i++)
{
int counter = 0;
// Each lambda captures its own instance of counter
counters.Add(() => ++counter);
}
return counters;
}
// Usage
public static void ClosureExample()
{
var counters = CreateCounters(2);
Console.WriteLine(counters[0]()); // 1
Console.WriteLine(counters[0]()); // 2
Console.WriteLine(counters[1]()); // 1
Console.WriteLine(counters[1]()); // 2
}
7.2.2.3 - Common Lambda Use Cases
// LINQ queries
var numbers = new[] { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
// Sorting
var people = GetPeople();
var sortedByAge = people.OrderBy(p => p.Age);
// Event handlers
button.Click += (sender, e) => {
MessageBox.Show("Button clicked!");
};
// Asynchronous programming
Task.Run(() => {
// Long-running operation
});
7.2.3 - Reflection and Attributes
Reflection allows programs to inspect and manipulate types, methods, and fields at runtime, while attributes provide a way to add metadata to code elements.
7.2.3.1 - Basic Reflection
// Get type information
Type stringType = typeof(string);
Console.WriteLine($"Type name: {stringType.Name}");
Console.WriteLine($"Full name: {stringType.FullName}");
Console.WriteLine($"Is class: {stringType.IsClass}");
// Get type from an object
object obj = "Hello";
Type objType = obj.GetType();
// Get methods
MethodInfo[] methods = stringType.GetMethods();
foreach (var method in methods)
{
Console.WriteLine($"Method: {method.Name}");
}
// Get properties
PropertyInfo[] properties = stringType.GetProperties();
foreach (var property in properties)
{
Console.WriteLine($"Property: {property.Name}, Type: {property.PropertyType.Name}");
}
7.2.3.2 - Dynamic Object Creation and Method Invocation
// Create an instance dynamically
Type listType = typeof(List<>).MakeGenericType(typeof(string));
object listInstance = Activator.CreateInstance(listType);
// Invoke a method dynamically
MethodInfo addMethod = listType.GetMethod("Add");
addMethod.Invoke(listInstance, new object[] { "Hello" });
addMethod.Invoke(listInstance, new object[] { "World" });
// Get the count
PropertyInfo countProperty = listType.GetProperty("Count");
int count = (int)countProperty.GetValue(listInstance); // 2
7.2.3.3 - Custom Attributes
// Define a custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Date { get; }
public AuthorAttribute(string name, string date)
{
Name = name;
Date = date;
}
}
// Apply the attribute
[Author("John Doe", "2023-01-15")]
public class SampleClass
{
[Author("Jane Smith", "2023-02-20")]
public void SampleMethod()
{
// Method implementation
}
}
// Read the attribute
public static void AttributeExample()
{
Type type = typeof(SampleClass);
// Get class attributes
var classAttributes = type.GetCustomAttributes(typeof(AuthorAttribute), false);
foreach (AuthorAttribute attr in classAttributes)
{
Console.WriteLine($"Class author: {attr.Name}, Date: {attr.Date}");
}
// Get method attributes
MethodInfo method = type.GetMethod("SampleMethod");
var methodAttributes = method.GetCustomAttributes(typeof(AuthorAttribute), false);
foreach (AuthorAttribute attr in methodAttributes)
{
Console.WriteLine($"Method author: {attr.Name}, Date: {attr.Date}");
}
}
7.2.3.4 - Built-in Attributes
C# provides many built-in attributes for various purposes:
// Obsolete attribute marks a code element as deprecated
[Obsolete("This method is deprecated. Use NewMethod instead.")]
public void OldMethod()
{
// Implementation
}
// Conditional attribute controls compilation based on preprocessor symbols
[Conditional("DEBUG")]
public void DebugMethod()
{
Console.WriteLine("This only runs in debug builds");
}
// Serializable attribute marks a class as serializable
[Serializable]
public class User
{
public string Name { get; set; }
[NonSerialized] // Field won't be serialized
private string _password;
}
7.2.4 - Dynamic Language Runtime (DLR)
The Dynamic Language Runtime (DLR) adds dynamic typing capabilities to C#, enabling more flexible programming styles and interoperability with dynamic languages.
7.2.4.1 - The dynamic Keyword
// Using dynamic to bypass compile-time type checking
dynamic dynamicVar = 10;
Console.WriteLine(dynamicVar.GetType()); // System.Int32
dynamicVar = "Hello";
Console.WriteLine(dynamicVar.GetType()); // System.String
// Method calls resolved at runtime
dynamicVar = new List<int> { 1, 2, 3 };
Console.WriteLine(dynamicVar.Count); // 3
7.2.4.2 - Interoperability with COM and Dynamic Languages
// COM interop example (Excel automation)
public static void ExcelExample()
{
dynamic excel = Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application"));
excel.Visible = true;
dynamic workbook = excel.Workbooks.Add();
dynamic worksheet = workbook.ActiveSheet;
worksheet.Cells[1, 1].Value = "Hello";
worksheet.Cells[1, 2].Value = "World";
// Clean up
workbook.Close(false);
excel.Quit();
}
7.2.4.3 - ExpandoObject
ExpandoObject
allows adding and removing members at runtime:
dynamic person = new System.Dynamic.ExpandoObject();
// Add properties dynamically
person.Name = "John";
person.Age = 30;
// Add a method dynamically
person.Greet = (Action)(() => Console.WriteLine($"Hello, my name is {person.Name}"));
// Use the dynamic object
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
person.Greet(); // Hello, my name is John
// Add and remove properties
person.Occupation = "Developer";
Console.WriteLine(person.Occupation); // Developer
// Cast to IDictionary to manipulate members
var personDict = (IDictionary<string, object>)person;
personDict.Remove("Occupation");
// This would now throw a RuntimeBinderException
// Console.WriteLine(person.Occupation);
7.2.5 - Expression Trees
Expression trees represent code as data structures, enabling runtime code generation and manipulation, particularly useful for LINQ providers and dynamic queries.
7.2.5.1 - Creating Expression Trees
// Expression tree from a lambda expression
Expression<Func<int, bool>> isPositive = x => x > 0;
// Manually building an expression tree
ParameterExpression parameter = Expression.Parameter(typeof(int), "x");
ConstantExpression zero = Expression.Constant(0);
BinaryExpression comparison = Expression.GreaterThan(parameter, zero);
Expression<Func<int, bool>> manualIsPositive =
Expression.Lambda<Func<int, bool>>(comparison, parameter);
7.2.5.2 - Analyzing Expression Trees
public static void AnalyzeExpression(Expression<Func<int, bool>> expression)
{
Console.WriteLine($"Expression: {expression}");
// Get the body of the expression
if (expression.Body is BinaryExpression binaryExpr)
{
Console.WriteLine($"NodeType: {binaryExpr.NodeType}");
Console.WriteLine($"Left: {binaryExpr.Left}");
Console.WriteLine($"Right: {binaryExpr.Right}");
}
}
// Usage
AnalyzeExpression(x => x > 0);
7.2.5.3 - Compiling and Executing Expression Trees
// Create an expression tree
Expression<Func<int, int, int>> addExpr = (x, y) => x + y;
// Compile the expression tree into a delegate
Func<int, int, int> addFunc = addExpr.Compile();
// Execute the compiled delegate
int result = addFunc(5, 3); // 8
7.2.5.4 - Dynamic Query Building
public static IQueryable<T> WhereGreaterThan<T, TValue>(
this IQueryable<T> source,
Expression<Func<T, TValue>> valueSelector,
TValue threshold) where TValue : IComparable<TValue>
{
// Get the parameter from the valueSelector expression
ParameterExpression parameter = valueSelector.Parameters[0];
// Create a comparison expression: valueSelector(x) > threshold
Expression comparison = Expression.GreaterThan(
valueSelector.Body,
Expression.Constant(threshold, typeof(TValue))
);
// Create a lambda expression: x => valueSelector(x) > threshold
var lambda = Expression.Lambda<Func<T, bool>>(comparison, parameter);
// Apply the where clause
return source.Where(lambda);
}
// Usage
var products = GetProducts().AsQueryable();
var expensiveProducts = products.WhereGreaterThan(p => p.Price, 100m);
7.2.6 - Dependency Injection
Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for resolving dependencies, promoting loose coupling and testability.
7.2.6.1 - Basic Principles
// Interface defining a service
public interface ILogger
{
void Log(string message);
}
// Implementation of the service
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
// Class with a dependency
public class OrderProcessor
{
private readonly ILogger _logger;
// Constructor injection
public OrderProcessor(ILogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void ProcessOrder(string orderId)
{
_logger.Log($"Processing order: {orderId}");
// Order processing logic
}
}
// Usage
public static void DIExample()
{
// Manual dependency injection
ILogger logger = new ConsoleLogger();
var processor = new OrderProcessor(logger);
processor.ProcessOrder("12345");
}
7.2.6.2 - Using a DI Container (Microsoft.Extensions.DependencyInjection)
using Microsoft.Extensions.DependencyInjection;
public static void DIContainerExample()
{
// Create service collection
var services = new ServiceCollection();
// Register services
services.AddSingleton<ILogger, ConsoleLogger>();
services.AddTransient<OrderProcessor>();
// Build service provider
var serviceProvider = services.BuildServiceProvider();
// Resolve and use services
var processor = serviceProvider.GetRequiredService<OrderProcessor>();
processor.ProcessOrder("12345");
}
7.2.6.3 - Service Lifetimes
// Configure services with different lifetimes
services.AddTransient<ITransientService, TransientService>(); // New instance each time
services.AddScoped<IScopedService, ScopedService>(); // Same within a scope
services.AddSingleton<ISingletonService, SingletonService>(); // Same for the application
7.2.7 - Caller Information Attributes
Caller information attributes provide a way to obtain information about the caller of a method without using reflection.
public static void LogMessage(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Console.WriteLine($"Message: {message}");
Console.WriteLine($"Member: {memberName}");
Console.WriteLine($"File: {sourceFilePath}");
Console.WriteLine($"Line: {sourceLineNumber}");
}
// Usage
public static void CallerInfoExample()
{
LogMessage("Something happened");
// Output:
// Message: Something happened
// Member: CallerInfoExample
// File: C:\Project\Program.cs
// Line: 42
}
7.2.8 - Span<T>
and Memory<T>
Span<T>
and Memory<T>
are high-performance types for working with contiguous memory regions, enabling efficient memory operations with minimal allocations.
7.2.8.1 - Span<T> Basics
// Create a span from an array
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array;
// Create a span from a part of an array
Span<int> slice = array.AsSpan(1, 3); // { 2, 3, 4 }
// Modify the span (also modifies the underlying array)
slice[0] = 22;
Console.WriteLine(array[1]); // 22
// Create a span on the stack
Span<byte> stackSpan = stackalloc byte[100];
// Fill with a value
stackSpan.Fill(0);
7.2.8.2 - Memory<T>
for Asynchronous Operations
public static async Task ProcessDataAsync(Memory<byte> buffer)
{
// Memory<T> can be used in async methods
await FillBufferAsync(buffer);
// Convert to Span for synchronous operations
Span<byte> span = buffer.Span;
ProcessSpan(span);
}
private static async Task FillBufferAsync(Memory<byte> buffer)
{
using var stream = new FileStream("data.bin", FileMode.Open);
await stream.ReadAsync(buffer);
}
private static void ProcessSpan(Span<byte> span)
{
// Process the data
for (int i = 0; i < span.Length; i++)
{
span[i] = (byte)(span[i] * 2);
}
}
7.2.8.3 - String Operations with Span
public static bool TryParseInt(ReadOnlySpan<char> input, out int result)
{
result = 0;
for (int i = 0; i < input.Length; i++)
{
if (!char.IsDigit(input[i]))
return false;
result = result * 10 + (input[i] - '0');
}
return true;
}
// Usage
string numberStr = "12345";
if (TryParseInt(numberStr.AsSpan(), out int number))
{
Console.WriteLine($"Parsed: {number}");
}
7.2.9 - Pattern Matching Enhancements (C# 9+)
C# 9 and later versions introduced significant enhancements to pattern matching, making code more expressive and concise.
For basic pattern matching with objects in an OOP context, see Section 3.6.8 - Pattern Matching with Objects in the Object-Oriented Programming chapter.
7.2.9.1 - Type Patterns
public static string Describe(object obj)
{
return obj switch
{
string s => $"A string of length {s.Length}",
int i => $"An integer with value {i}",
double d => $"A double with value {d}",
Person p => $"A person named {p.Name}",
null => "A null value",
_ => $"Something else: {obj.GetType().Name}"
};
}
7.2.9.2 - Property Patterns
public static string ClassifyPerson(Person person)
{
return person switch
{
{ Age: < 18 } => "Minor",
{ Age: >= 18 and < 65 } => "Adult",
{ Age: >= 65 } => "Senior",
_ => "Unknown"
};
}
7.2.9.3 - Tuple Patterns
public static string Classify(int x, int y)
{
return (x, y) switch
{
(0, 0) => "Origin",
(_, 0) => "X-axis",
(0, _) => "Y-axis",
(var a, var b) when a == b => "Diagonal",
(var a, var b) when a == -b => "Anti-diagonal",
_ => "Elsewhere"
};
}
7.2.9.4 - Positional Patterns
public record Point(int X, int Y);
public static string ClassifyPoint(Point point)
{
return point switch
{
(0, 0) => "Origin",
(_, 0) => "X-axis",
(0, _) => "Y-axis",
(var x, var y) when x == y => "Diagonal",
_ => "Elsewhere"
};
}
7.2.9.5 - List Patterns (C# 11+)
public static string DescribeList(int[] numbers)
{
return numbers switch
{
[] => "Empty array",
[1, 2, 3] => "Array with exactly 1, 2, 3",
[1, 2, ..] => "Array starting with 1, 2",
[.., 98, 99] => "Array ending with 98, 99",
[1, .., 99] => "Array starting with 1 and ending with 99",
[var first, .., var last] => $"Array starting with {first} and ending with {last}",
_ => "Some other array"
};
}
7.2.10 - Init-only Properties (C# 9+)
Init-only properties allow immutable objects with more flexible initialization syntax.
public class Person
{
// Regular property
public string Name { get; set; }
// Init-only property
public DateTime DateOfBirth { get; init; }
// Read-only property
public int Age => DateTime.Now.Year - DateOfBirth.Year;
}
// Usage
var person = new Person
{
Name = "John",
DateOfBirth = new DateTime(1990, 1, 1)
};
// This works
person.Name = "John Doe";
// This would cause a compile-time error
// person.DateOfBirth = new DateTime(1991, 1, 1);
7.2.11 - Record Types (C# 9+)
Records are reference types with value-based equality semantics, designed for immutable data models.
7.2.11.1 - Basic Record Syntax
// Record declaration
public record Person(string FirstName, string LastName, int Age);
// Usage
var person1 = new Person("John", "Doe", 30);
var person2 = new Person("John", "Doe", 30);
Console.WriteLine(person1 == person2); // True (value-based equality)
Console.WriteLine(person1.ToString()); // Person { FirstName = John, LastName = Doe, Age = 30 }
// Non-destructive mutation
var person3 = person1 with { Age = 31 };
Console.WriteLine(person3); // Person { FirstName = John, LastName = Doe, Age = 31 }
7.2.11.2 - Record Classes vs. Record Structs (C# 10+)
// Record class (reference type)
public record class PersonClass(string Name, int Age);
// Record struct (value type)
public record struct PersonStruct(string Name, int Age);
7.2.11.3 - Records with Additional Members
public record Employee(string Name, string Department, decimal Salary)
{
// Additional properties
public string FullTitle => $"{Name}, {Department}";
// Methods
public decimal CalculateBonus(decimal rate) => Salary * rate;
// Custom constructor
public Employee(string name, string department)
: this(name, department, 50000m)
{
}
}
7.2.12 - Global Using Directives (C# 10+)
Global using directives allow you to specify using directives that apply to all files in a project.
// In a file named GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using MyCompany.MyProject.Models;
// Now these namespaces are available in all files without explicit using directives
7.2.13 - File-scoped Namespaces (C# 10+)
File-scoped namespaces reduce indentation and simplify code organization.
// Traditional namespace declaration
namespace MyCompany.MyProject
{
public class MyClass
{
// Class implementation
}
}
// File-scoped namespace (C# 10+)
namespace MyCompany.MyProject;
public class MyClass
{
// Class implementation
}
7.2.14 - Required Members (C# 11+)
Required members ensure that specific properties must be initialized during object creation.
For basic usage of Required Members in class definitions, see Section 3.1.12 - Required Members (C# 11+) in the Object-Oriented Programming chapter.
public class Person
{
// Must be initialized during object creation
public required string FirstName { get; set; }
public required string LastName { get; set; }
// Optional property
public int? Age { get; set; }
}
// Usage
var person = new Person
{
FirstName = "John",
LastName = "Doe"
// Age is optional
};
// This would cause a compile-time error
// var invalidPerson = new Person { FirstName = "John" };
7.2.15 - Raw String Literals (C# 11+)
Raw string literals simplify working with multi-line strings and strings containing quotes or special characters.
// Basic raw string literal
string json = """
{
"name": "John Doe",
"age": 30,
"isActive": true
}
""";
// Raw string with interpolation
string name = "John";
int age = 30;
string interpolatedJson = $$"""
{
"name": "{{name}}",
"age": {{age}},
"isActive": true
}
""";
7.2.16 - Collection Expressions (C# 12+)
Collection expressions provide a concise syntax for creating and initializing collections.
// Array creation
int[] numbers = [1, 2, 3, 4, 5];
// List creation
List<string> names = ["Alice", "Bob", "Charlie"];
// Spread operator
int[] moreNumbers = [0, ..numbers, 6]; // [0, 1, 2, 3, 4, 5, 6]
// Dictionary creation
Dictionary<string, int> ages = ["Alice": 30, "Bob": 25, "Charlie": 35];
7.2.17 - Primary Constructors (C# 12+)
Primary constructors simplify class definitions by allowing constructor parameters directly in the class declaration.
For basic usage of Primary Constructors in class definitions, see Section 3.1.13 - Primary Constructors (C# 12+) in the Object-Oriented Programming chapter.
// Class with primary constructor
public class Person(string name, int age)
{
// Constructor parameters are accessible throughout the class
public string Name { get; } = name;
public int Age { get; } = age;
public string Greet() => $"Hello, my name is {name} and I'm {age} years old.";
}
// Usage
var person = new Person("John", 30);
Console.WriteLine(person.Greet());
7.2.18 - Interceptors (C# 13)
Interceptors allow you to intercept method calls at compile time, enabling advanced scenarios like aspect-oriented programming, cross-cutting concerns, and more. This feature is part of C# 13 and represents a significant advancement in compile-time metaprogramming capabilities.
Interceptors are a preview feature in C# 13. The syntax and capabilities may change in future releases.
7.2.18.1 - Basic Interceptor Syntax
The basic syntax for interceptors involves:
- Creating an interceptor method with the appropriate attribute
- Specifying the target method call to intercept
// Note: This is a preview feature in C# 13
using System.Runtime.CompilerServices;
public static class LoggingInterceptor
{
[InterceptsLocation("Program.cs", line: 10, character: 12)]
public static void InterceptMethod(string message)
{
Console.WriteLine($"[LOGGED] {DateTime.Now}: About to log message");
Console.WriteLine(message);
}
}
// In Program.cs, line 10
public static void LogMessage(string message)
{
Console.WriteLine(message); // This call will be intercepted
}
7.2.18.2 - Interceptor Attributes
C# 13 introduces several attributes for interceptors:
// Intercept by source code location
[InterceptsLocation(string filePath, int line, int character)]
// Intercept by method signature
[InterceptsMethod(Type declaringType, string methodName)]
// Intercept by specific method
[InterceptsMethod(MethodInfo method)]
7.2.18.3 - Use Cases for Interceptors
Logging and Telemetry
public static class TelemetryInterceptor
{
[InterceptsMethod(typeof(ApiClient), "SendRequest")]
public static async Task<ApiResponse> InterceptApiCall(ApiClient client, string endpoint, object data)
{
var stopwatch = Stopwatch.StartNew();
Console.WriteLine($"[API CALL] Starting request to {endpoint}");
try
{
// Call the original method
var result = await client.SendRequest(endpoint, data);
Console.WriteLine($"[API CALL] Completed request to {endpoint} in {stopwatch.ElapsedMilliseconds}ms");
return result;
}
catch (Exception ex)
{
Console.WriteLine($"[API CALL] Failed request to {endpoint}: {ex.Message}");
throw;
}
}
}
Caching
public static class CachingInterceptor
{
private static readonly Dictionary<string, object> _cache = new();
[InterceptsMethod(typeof(DataService), "GetData")]
public static async Task<DataResult> InterceptGetData(DataService service, string key)
{
// Check if the result is in the cache
string cacheKey = $"GetData_{key}";
if (_cache.TryGetValue(cacheKey, out var cachedResult))
{
Console.WriteLine($"Cache hit for key: {key}");
return (DataResult)cachedResult;
}
// Call the original method
var result = await service.GetData(key);
// Cache the result
_cache[cacheKey] = result;
Console.WriteLine($"Cached result for key: {key}");
return result;
}
}
Authorization
public static class AuthorizationInterceptor
{
[InterceptsMethod(typeof(SecureService), "PerformSecureOperation")]
public static void InterceptSecureOperation(SecureService service, User user, string operation)
{
// Check if the user is authorized
if (!IsAuthorized(user, operation))
{
throw new UnauthorizedAccessException($"User {user.Name} is not authorized to perform {operation}");
}
// Call the original method
service.PerformSecureOperation(user, operation);
}
private static bool IsAuthorized(User user, string operation)
{
// Authorization logic
return user.Roles.Contains("Admin") || user.Permissions.Contains(operation);
}
}
7.2.18.4 - Limitations and Considerations
When working with interceptors, keep these considerations in mind:
-
Compile-Time Only: Interceptors work at compile time, not runtime, so they can't intercept dynamic method calls.
-
Method Signature Matching: The interceptor method must match the signature of the intercepted method (including return type).
-
Performance Impact: Since interception happens at compile time, there's no runtime performance penalty.
-
Debugging Complexity: Intercepted code can be harder to debug since the actual execution path is modified.
-
Visibility: Interceptors can only intercept methods they have access to (public, internal with appropriate access).
7.2.18.5 - Comparison with Other Approaches
Approach | When to Use | Pros | Cons |
---|---|---|---|
Interceptors | Cross-cutting concerns, AOP | Compile-time, no runtime overhead | Preview feature, limited to compile-time |
7.2.19 - Params Collections (C# 13)
C# 13 extends the params
modifier to work with a variety of collection types beyond arrays, making it more flexible and efficient for handling variable-length argument lists.
7.2.19.1 - Understanding Params Collections
Before C# 13, the params
modifier could only be used with arrays. Now, it can be used with:
Span<T>
andReadOnlySpan<T>
- Collection interfaces like
IEnumerable<T>
,IReadOnlyCollection<T>
,IReadOnlyList<T>
,ICollection<T>
, andIList<T>
- Any concrete collection type that implements
IEnumerable<T>
and has anAdd
method
// Traditional params with arrays (pre-C# 13)
public void TraditionalParams(params int[] numbers)
{
foreach (var number in numbers)
{
Console.Write($"{number} ");
}
Console.WriteLine();
}
// C# 13: params with Span<T>
public void SpanParams(params Span<int> numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.Write($"{numbers[i]} ");
}
Console.WriteLine();
}
// C# 13: params with ReadOnlySpan<T>
public void ReadOnlySpanParams(params ReadOnlySpan<int> numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
Console.Write($"{numbers[i]} ");
}
Console.WriteLine();
}
// C# 13: params with IEnumerable<T>
public void EnumerableParams(params IEnumerable<int> collections)
{
foreach (var collection in collections)
{
foreach (var item in collection)
{
Console.Write($"{item} ");
}
Console.WriteLine();
}
}
7.2.19.2 - Performance Benefits
Using params
with Span<T>
or ReadOnlySpan<T>
can provide significant performance benefits by avoiding heap allocations:
// Benchmark example
public class ParamsBenchmark
{
// Traditional array-based params (allocates an array on the heap)
public int SumArray(params int[] numbers)
{
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
return sum;
}
// Span-based params (stack-allocated, no heap allocation)
public int SumSpan(params Span<int> numbers)
{
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i];
}
return sum;
}
}
// Usage
var benchmark = new ParamsBenchmark();
benchmark.SumArray(1, 2, 3, 4, 5); // Allocates an array on the heap
benchmark.SumSpan(1, 2, 3, 4, 5); // No heap allocation
7.2.19.3 - Interface-Based Params
When using interface types with params
, the compiler synthesizes the appropriate storage:
// Using interface-based params
public void ProcessItems<T>(params IReadOnlyList<T> items)
{
foreach (var item in items)
{
Console.WriteLine($"Processing item: {item}");
}
}
// Usage
List<string> list1 = new List<string> { "apple", "banana" };
List<string> list2 = new List<string> { "cherry", "date" };
// Pass multiple collections
ProcessItems(list1, list2);
// Pass individual items (compiler creates a collection)
ProcessItems("grape", "kiwi", "lemon");
7.2.19.4 - Practical Applications
The extended params
feature is particularly useful for:
- High-performance code: Using
Span<T>
to avoid allocations - API design: Creating more flexible methods that can accept various collection types
- Data processing: Working with multiple collections efficiently
// Example: Text processing utility
public class TextProcessor
{
// Process multiple text blocks efficiently
public string Concatenate(params ReadOnlySpan<char> textBlocks)
{
// Calculate total length
int totalLength = 0;
for (int i = 0; i < textBlocks.Length; i++)
{
totalLength += textBlocks[i].Length;
}
// Create a single result without intermediate allocations
Span<char> result = new char[totalLength];
int position = 0;
for (int i = 0; i < textBlocks.Length; i++)
{
textBlocks[i].CopyTo(result.Slice(position));
position += textBlocks[i].Length;
}
return new string(result);
}
}
// Usage
var processor = new TextProcessor();
string result = processor.Concatenate("Hello, ", "world", "!");
Console.WriteLine(result); // "Hello, world!"
7.2.19.5 - Limitations and Considerations
When using the extended params
feature, keep these considerations in mind:
- Type Compatibility: The argument types must be compatible with the parameter type.
- Performance Implications: While
Span<T>
can improve performance, interface-based params may still involve allocations. - Method Resolution: Methods with more specific parameter types are preferred during overload resolution.
// Example of method overloading with params
public class OverloadExample
{
// Most specific version
public void Process(params Span<int> numbers)
{
Console.WriteLine("Processing with Span<int>");
}
// Less specific version
public void Process(params IEnumerable<int> numbers)
{
Console.WriteLine("Processing with IEnumerable<int>");
}
// Least specific version
public void Process(params object[] objects)
{
Console.WriteLine("Processing with object[]");
}
}
// Usage
var example = new OverloadExample();
example.Process(1, 2, 3); // Calls the Span<int> version
7.2.20 - Extension Members (C# 14)
C# 14 introduces a significant enhancement to extension methods with the new extension members feature. This allows developers to define not just extension methods, but also extension properties, indexers, and even static extension members.
7.2.20.1 - Extension Blocks
The core of the extension members feature is the new extension
keyword and extension block syntax:
public static class StringExtensions
{
// Extension block for string type
extension(string str)
{
// Extension property
public int WordCount => str.Split(new[] { ' ', '\t', '\n', '\r' },
StringSplitOptions.RemoveEmptyEntries).Length;
// Extension method
public string Truncate(int maxLength) =>
str.Length <= maxLength ? str : str.Substring(0, maxLength) + "...";
// Extension indexer
public char this[Index index] => str[index];
}
// Static extension block
extension(string)
{
// Static extension method
public static string Combine(string first, string second) => $"{first} {second}";
// Static extension property
public static string Empty => "";
}
}
7.2.20.2 - Instance Extension Members
Instance extension members are called on instances of the extended type, similar to traditional extension methods:
// Using extension properties and methods
string text = "This is a sample text for demonstration purposes.";
Console.WriteLine($"Word count: {text.WordCount}"); // Output: Word count: 8
Console.WriteLine($"Truncated: {text.Truncate(10)}"); // Output: Truncated: This is a...
// Using extension indexers
Console.WriteLine($"First character: {text[0]}"); // Output: First character: T
Console.WriteLine($"Last character: {text[^1]}"); // Output: Last character: .
7.2.20.3 - Static Extension Members
Static extension members are called on the type itself, not on instances:
// Using static extension methods
string combined = string.Combine("Hello", "World");
Console.WriteLine(combined); // Output: Hello World
// Using static extension properties
string empty = string.Empty;
Console.WriteLine($"Empty string length: {empty.Length}"); // Output: Empty string length: 0
7.2.20.4 - Generic Extension Members
Extension members can be generic, allowing for more flexible and reusable code:
public static class EnumerableExtensions
{
// Generic extension block
extension<T>(IEnumerable<T> source)
{
// Extension property
public bool IsEmpty => !source.Any();
// Extension method with type parameter
public IEnumerable<TResult> SelectNonNull<TResult>(Func<T, TResult> selector)
where TResult : class
{
foreach (var item in source)
{
var result = selector(item);
if (result != null)
yield return result;
}
}
// Extension indexer
public T this[int index] => source.ElementAt(index);
}
// Static generic extension block
extension<T>(IEnumerable<T>)
{
// Static extension method
public static IEnumerable<T> Empty => Enumerable.Empty<T>();
// Static extension method with parameters
public static IEnumerable<T> Create(params T[] items) => items;
}
}
7.2.20.5 - Practical Applications
Extension members are particularly useful for:
- API Enhancement: Adding functionality to types you don't own
- Domain-Specific Languages: Creating fluent interfaces
- Code Organization: Keeping related functionality together
- Testing: Adding test-specific functionality to production code
// Example: Financial calculations for decimal values
public static class FinancialExtensions
{
extension(decimal amount)
{
// Extension properties for financial calculations
public decimal WithVAT => amount * 1.2m;
public decimal WithDiscount(decimal discountPercentage) =>
amount * (1 - discountPercentage / 100);
// Extension method for formatting
public string AsCurrency(string currencySymbol = "$") =>
$"{currencySymbol}{amount:N2}";
}
// Static extensions for decimal
extension(decimal)
{
// Static extension method for parsing
public static bool TryParseCurrency(string input, out decimal result)
{
// Remove currency symbols and separators
string cleaned = new string(input.Where(c => char.IsDigit(c) || c == '.' || c == ',').ToArray());
return decimal.TryParse(cleaned, out result);
}
}
}
// Usage
decimal price = 99.99m;
Console.WriteLine($"Price: {price.AsCurrency()}");
Console.WriteLine($"Price with VAT: {price.WithVAT.AsCurrency()}");
Console.WriteLine($"Price with 10% discount: {price.WithDiscount(10).AsCurrency()}");
// Using static extension method
if (decimal.TryParseCurrency("$123.45", out decimal parsedAmount))
{
Console.WriteLine($"Parsed amount: {parsedAmount}");
}
7.2.21 - Null-conditional Assignment (C# 14)
C# 14 introduces null-conditional assignment, which allows you to use the null-conditional operators (?.
and ?[]
) on the left side of assignment operations.
7.2.21.1 - Basic Usage
The null-conditional assignment operators allow you to assign a value only if the target object is not null:
// Traditional null check before assignment
if (customer != null)
{
customer.Name = "John Doe";
}
// With null-conditional assignment
customer?.Name = "John Doe";
7.2.21.2 - Working with Properties and Fields
Null-conditional assignment works with properties, fields, and indexers:
public class Customer
{
public string Name { get; set; }
public Address BillingAddress { get; set; }
public List<Order> Orders { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
public class Order
{
public string OrderId { get; set; }
public decimal Amount { get; set; }
}
// Usage
Customer customer = GetCustomer(); // May return null
// Property assignment
customer?.Name = "John Doe";
// Nested property assignment
customer?.BillingAddress?.Street = "123 Main St";
// Indexer assignment
customer?.Orders?[0].OrderId = "ORD-12345";
7.2.21.3 - Compound Assignment Operators
Null-conditional assignment also works with compound assignment operators (+=
, -=
, *=
, etc.):
// Traditional approach
if (order != null)
{
order.Amount += 10.0m;
}
// With null-conditional compound assignment
order?.Amount += 10.0m;
// Other compound operators
counter?.Value *= 2;
text?.Length -= 5;
flags?.Value |= 0x04;
7.2.21.4 - Practical Applications
Null-conditional assignment is particularly useful for:
- Simplifying Null Checks: Reducing boilerplate code
- Working with Optional Data: Handling potentially null objects gracefully
- Configuration Updates: Setting values only when the configuration object exists
// Example: Configuration update
public class ConfigurationManager
{
private AppSettings _settings;
public void UpdateDatabaseConnection(string connectionString)
{
// Update only if settings object exists
_settings?.Database?.ConnectionString = connectionString;
}
public void IncrementRetryCount()
{
// Increment only if settings object exists
_settings?.Network?.RetryCount += 1;
}
public void EnableFeature(string featureName)
{
// Add to enabled features if the collection exists
_settings?.Features?.EnabledFeatures?.Add(featureName);
}
}
7.2.21.5 - Limitations and Considerations
When using null-conditional assignment, keep these considerations in mind:
- Expression Evaluation: The right side of the assignment is not evaluated if the left side is null.
- No Return Value: Unlike other null-conditional operations, null-conditional assignment doesn't return a value.
- Not for Initialization: Cannot be used to initialize a null object, only to update an existing object.
// Right side not evaluated if left side is null
customer?.Name = GetExpensiveComputationResult(); // GetExpensiveComputationResult() not called if customer is null
// Cannot use for initialization
Customer customer = null;
customer?.Name = "John"; // customer remains null
// Must initialize first
customer = new Customer();
customer?.Name = "John"; // Now it works
7.2.22 - Nameof Enhancements (C# 14)
C# 14 enhances the nameof
operator to support unbound generic types, making it more flexible and useful in generic programming scenarios.
7.2.22.1 - Using nameof with Unbound Generic Types
Before C# 14, nameof
could only be used with closed generic types (types with specific type arguments). Now, it can be used with unbound generic types:
// C# 14: nameof with unbound generic types
string listName = nameof(List<>); // "List"
string dictionaryName = nameof(Dictionary<,>); // "Dictionary"
string keyValuePairName = nameof(KeyValuePair<,>); // "KeyValuePair"
// Previously, you had to use closed generic types
string listNameOld = nameof(List<int>); // "List"
7.2.22.2 - Practical Applications
This enhancement is particularly useful for:
- Generic Type Factories: Creating instances of generic types dynamically
- Reflection: Working with generic type definitions
- Logging and Diagnostics: Reporting type information
- Code Generation: Generating code for generic types
// Example: Generic type factory
public class GenericTypeFactory
{
public object CreateInstance(Type genericTypeDefinition, Type[] typeArguments)
{
if (!genericTypeDefinition.IsGenericTypeDefinition)
{
throw new ArgumentException(
$"{nameof(genericTypeDefinition)} must be a generic type definition");
}
Type constructedType = genericTypeDefinition.MakeGenericType(typeArguments);
return Activator.CreateInstance(constructedType);
}
public void LogGenericTypeInfo(Type genericTypeDefinition)
{
string typeName = nameof(genericTypeDefinition);
Console.WriteLine($"Working with generic type: {typeName}");
// With C# 14, we can use unbound generic types directly
if (genericTypeDefinition == typeof(List<>))
{
Console.WriteLine($"Creating a {nameof(List<>)}");
}
else if (genericTypeDefinition == typeof(Dictionary<,>))
{
Console.WriteLine($"Creating a {nameof(Dictionary<,>)}");
}
}
}
7.2.22.3 - Combining with Other C# Features
The enhanced nameof
operator works well with other C# features:
// Using with generic constraints
public class GenericConstraintExample<T> where T : IComparable<T>
{
public void LogTypeInfo()
{
Console.WriteLine($"Type parameter: {typeof(T).Name}");
Console.WriteLine($"Constraint interface: {nameof(IComparable<>)}");
}
}
// Using with reflection
public static Type GetGenericTypeDefinition<T>()
{
Type type = typeof(T);
if (type.IsGenericType)
{
Console.WriteLine($"Generic type name: {nameof(type.GetGenericTypeDefinition())}");
return type.GetGenericTypeDefinition();
}
return null;
}
// Using with attributes
[TypeConverter(typeof(ListConverter<>))]
public class CustomList<T>
{
public void LogConverterInfo()
{
Console.WriteLine($"Using converter: {nameof(ListConverter<>)}");
}
}
7.2.23 - Span Conversions (C# 14)
C# 14 introduces new implicit conversions for Span<T>
and ReadOnlySpan<T>
, making it easier and more natural to work with these high-performance types.
7.2.23.1 - New Implicit Conversions
The new implicit conversions include:
- From array types to
ReadOnlySpan<T>
- From array types to
Span<T>
(when the array is not covariant) - From
Span<T>
toReadOnlySpan<T>
- From string to
ReadOnlySpan<char>
// Array to ReadOnlySpan<T>
int[] numbers = { 1, 2, 3, 4, 5 };
ReadOnlySpan<int> span = numbers; // Implicit conversion
// Array to Span<T> (non-covariant case)
int[] mutableNumbers = { 1, 2, 3, 4, 5 };
Span<int> mutableSpan = mutableNumbers; // Implicit conversion
// Span<T> to ReadOnlySpan<T>
Span<int> originalSpan = stackalloc int[5];
ReadOnlySpan<int> readOnlySpan = originalSpan; // Implicit conversion
// String to ReadOnlySpan<char>
string text = "Hello, World!";
ReadOnlySpan<char> charSpan = text; // Implicit conversion
7.2.23.2 - Benefits in Method Calls
These conversions simplify method calls by eliminating the need for explicit conversions:
// Method that takes a ReadOnlySpan<char>
public int CountOccurrences(ReadOnlySpan<char> text, char character)
{
int count = 0;
for (int i = 0; i < text.Length; i++)
{
if (text[i] == character)
count++;
}
return count;
}
// Usage with different types
string message = "Hello, World!";
int occurrencesInString = CountOccurrences(message, 'l'); // Implicit conversion from string
char[] charArray = { 'H', 'e', 'l', 'l', 'o' };
int occurrencesInArray = CountOccurrences(charArray, 'l'); // Implicit conversion from array
Span<char> charSpan = stackalloc char[] { 'H', 'e', 'l', 'l', 'o' };
int occurrencesInSpan = CountOccurrences(charSpan, 'l'); // Implicit conversion from Span<T>
7.2.23.3 - Working with LINQ-like Extensions
The new conversions make it easier to use LINQ-like extension methods with spans:
public static class SpanExtensions
{
// Extension method for ReadOnlySpan<T>
public static bool Any<T>(this ReadOnlySpan<T> span, Func<T, bool> predicate)
{
for (int i = 0; i < span.Length; i++)
{
if (predicate(span[i]))
return true;
}
return false;
}
public static int Count<T>(this ReadOnlySpan<T> span, Func<T, bool> predicate)
{
int count = 0;
for (int i = 0; i < span.Length; i++)
{
if (predicate(span[i]))
count++;
}
return count;
}
}
// Usage with different types
int[] numbers = { 1, 2, 3, 4, 5 };
bool hasEven = numbers.Any(n => n % 2 == 0); // Implicit conversion from array
int evenCount = numbers.Count(n => n % 2 == 0);
Span<int> numberSpan = stackalloc int[] { 1, 2, 3, 4, 5 };
bool hasOdd = numberSpan.Any(n => n % 2 != 0); // Implicit conversion from Span<T>
int oddCount = numberSpan.Count(n => n % 2 != 0);
7.2.23.4 - Performance Considerations
These implicit conversions can improve performance by:
- Reducing Allocations: Avoiding the need for temporary arrays or collections
- Eliminating Copying: Working directly with the original memory
- Simplifying APIs: Creating more intuitive and consistent interfaces
// Performance-sensitive method using spans
public bool ContainsSubstring(ReadOnlySpan<char> text, ReadOnlySpan<char> substring)
{
if (substring.Length > text.Length)
return false;
for (int i = 0; i <= text.Length - substring.Length; i++)
{
bool found = true;
for (int j = 0; j < substring.Length; j++)
{
if (text[i + j] != substring[j])
{
found = false;
break;
}
}
if (found)
return true;
}
return false;
}
// Usage with different types
string haystack = "The quick brown fox jumps over the lazy dog";
string needle = "brown";
bool contains = ContainsSubstring(haystack, needle); // Both convert implicitly
char[] haystackArray = haystack.ToCharArray();
char[] needleArray = needle.ToCharArray();
bool containsFromArrays = ContainsSubstring(haystackArray, needleArray); // Both convert implicitly
7.2.23.5 - Limitations and Considerations
When using these implicit conversions, keep these considerations in mind:
- Array Covariance: Implicit conversion from array to
Span<T>
doesn't work with covariant arrays - Lifetime Management: Spans don't extend the lifetime of the data they reference
- Method Resolution: The new conversions may affect overload resolution
// Covariance limitation
object[] objects = new string[] { "a", "b", "c" };
// Span<object> objectSpan = objects; // Error: Cannot implicitly convert
// Lifetime management
ReadOnlySpan<char> GetFirstWord(string text)
{
int spaceIndex = text.IndexOf(' ');
return spaceIndex >= 0 ? text.AsSpan(0, spaceIndex) : text.AsSpan();
}
// Method resolution
public void Process(int[] array) { Console.WriteLine("Array version"); }
public void Process(ReadOnlySpan<int> span) { Console.WriteLine("Span version"); }
// In C# 14, this calls the Span version due to the new implicit conversion
Process(new int[] { 1, 2, 3 });
Summary
Advanced C# language features provide powerful tools for creating more expressive, maintainable, and efficient code. From delegates and lambda expressions to modern features like records and pattern matching, these capabilities enable developers to write cleaner code that better expresses intent while maintaining performance and type safety.