Senior C# Developer Acceleration
Advanced C# Language Features
Writing Non-Blocking Code
Modern applications, especially those with a user interface or that interact with web services, need to stay responsive. If a user clicks a button to download a file, the entire application shouldn't freeze while waiting for the download to finish. This is where asynchronous programming comes in.
In C#, this is handled gracefully with the async and await keywords. They allow you to write code that performs long-running operations, like network requests or file I/O, without blocking the main thread. This ensures the UI remains interactive and the application can handle other work.
The async keyword is a modifier you add to a method signature. It signals that the method can contain an await expression and will return a Task or Task<T>. The Task object represents the ongoing work. The await keyword is then used inside an async method to pause its execution until the awaited Task completes. While paused, control returns to the caller, allowing the application to continue running.
// A method that simulates a network call
public async Task<string> FetchDataAsync()
{
Console.WriteLine("Starting data fetch...");
// Simulate a network delay of 2 seconds without blocking
await Task.Delay(2000);
Console.WriteLine("Data fetched.");
return "Here is your data!";
}
// How you might call it
public async void OnButtonClick()
{
string data = await FetchDataAsync();
Console.WriteLine(data);
}
asyncandawaitmake asynchronous code look and feel almost like synchronous code, which dramatically simplifies writing and reading it.
Querying Data with LINQ
Before Language Integrated Query (LINQ), getting data from different sources meant learning different APIs. You'd use one library for databases, another for XML files, and standard loops for in-memory collections. LINQ changed that by providing a single, unified way to query data directly within C#.
LINQ lets you write expressive and readable queries against collections of objects, SQL databases, XML documents, and more. It makes your data-access code cleaner and more maintainable.
There are two ways to write LINQ queries: query syntax and method syntax. Query syntax looks a lot like SQL, which can make it intuitive for those with a database background. Method syntax uses a chain of standard methods, often combined with lambda expressions.
List<int> numbers = new List<int> { 5, 10, 8, 3, 6, 12 };
// 1. Query Syntax
var querySyntaxResult = from num in numbers
where num > 5
orderby num
select num;
// 2. Method Syntax (more common)
var methodSyntaxResult = numbers.Where(num => num > 5)
.OrderBy(num => num);
// Both queries will produce the same result: { 6, 8, 10, 12 }
Common LINQ methods include:
Where(): Filters a sequence based on a condition.Select(): Projects each element of a sequence into a new form.OrderBy(): Sorts the elements of a sequence.GroupBy(): Groups elements based on a key.First()/FirstOrDefault(): Returns the first element of a sequence, optionally matching a condition.
Delegates, Events, and Lambdas
Delegates, events, and lambda expressions are related concepts that form the basis for some of C#'s most powerful features, including LINQ and event-driven programming.
delegate
noun
A type that represents a reference to a method with a specific parameter list and return type. You can think of it as a type-safe function pointer.
Delegates allow you to treat methods as variables. You can pass them to other methods, store them in collections, and invoke them dynamically. This is the foundation for creating flexible and extensible code.
Events provide a structured way for a class to send notifications to other classes. They are a specialized form of delegate that restricts access, allowing outside code to only subscribe (+=) or unsubscribe (-=) to the notification, not to raise it directly. This publisher-subscriber pattern is fundamental to UI frameworks and many other application architectures.
public class Button
{
// 1. Define a delegate for the event
public delegate void ClickEventHandler(object sender, EventArgs e);
// 2. Define the event based on the delegate
public event ClickEventHandler Clicked;
// 3. A method to raise the event
public void SimulateClick()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
// In another class...
Button myButton = new Button();
// 4. Subscribe to the event
myButton.Clicked += (sender, e) => Console.WriteLine("Button was clicked!");
myButton.SimulateClick(); // Output: "Button was clicked!"
In the example above, you might have noticed the (sender, e) => ... part. That's a lambda expression. Lambda expressions are a concise way to write anonymous functions right where you need them. They are syntactic sugar that the compiler often turns into delegates or expression trees behind the scenes.
Without a lambda, you would have to define a separate, named method and then assign it to the event. Lambdas make the code much shorter and keep the logic right where it's being used.
// Using a lambda expression (concise)
List<int> evens = numbers.Where(n => n % 2 == 0);
// Using a named method (more verbose)
bool IsEven(int n)
{
return n % 2 == 0;
}
List<int> evens2 = numbers.Where(IsEven);
Lambda expressions are the glue that connects many advanced C# features, making syntax for things like LINQ queries and event handlers clean and expressive.
Mastering these features is a big step toward writing professional, efficient, and modern C# code. They enable you to build complex applications that are both performant and easy to maintain.