
LINQ (Language-Integrated Query) is a core feature of C# that revolutionizes how developers interact with data. It provides a unified querying syntax that can operate seamlessly across various data sources such as collections, databases, XML, and web APIs. By combining the power of functional and declarative programming, LINQ simplifies data manipulation and enhances code readability. This section explores LINQ’s syntax, operators, and practical applications to demonstrate its utility in modern development.
LINQ supports two primary syntaxes: query syntax and method syntax, catering to different developer preferences.
var adults = from person in people
where person.Age >= 18
select person;
Here, the query retrieves all people aged 18 and older in a way that is easy to understand and maintain.
var adults = people.Where(p => p.Age >= 18);
This syntax is concise and better suited for chaining multiple operations.
While both syntaxes achieve the same results, method syntax is generally favored in modern C# development due to its greater flexibility and direct integration with lambda expressions.
LINQ offers a rich set of operators to handle filtering, transformation, aggregation, and more. Below are some commonly used operators:
var names = people.Select(p => p.Name);
This query extracts only the Name property from the collection of people, returning a list of names.
var adults = people.Where(p => p.Age >= 18);
This example retrieves only the people who are 18 years old or older.
var groupedByAge = people.GroupBy(p => p.Age);
foreach (var group in groupedByAge)
{
Console.WriteLine($”Age: {group.Key}”);
foreach (var person in group)
{
Console.WriteLine(person.Name);
}
}
This query groups people by their age and allows iteration through each age group.
var innerJoin = people.Join(
orders,
person => person.Id,
order => order.PersonId,
(person, order) => new { person.Name, order.Total }
);
This query links people to their orders using their Id as the key, returning a combined result with names and order totals.
public static IEnumerable<string> StartsWith(this IEnumerable<string> source, char letter)
{
return source.Where(name => name.StartsWith(letter));
}
This operator can be used as:
var namesStartingWithA = names.StartsWith(‘A’);
LINQ’s versatility extends to numerous real-world scenarios, making it invaluable for developers. Here are some practical applications:
var sortedAdults = people
.Where(p => p.Age >= 18)
.OrderBy(p => p.Name);
This query retrieves adults and sorts them alphabetically by name.
var averageAge = people.Average(p => p.Age);
Console.WriteLine($”Average Age: {averageAge}”);
var ageSummary = people.GroupBy(p => p.Age)
.Select(g => new { Age = g.Key, Count = g.Count() });
foreach (var group in ageSummary)
{
Console.WriteLine($”Age: {group.Age}, Count: {group.Count}”);
}
var allOrders = customers.SelectMany(c => c.Orders);
var products = dbContext.Products.Where(p => p.Price > 50).ToList();
Asynchronous programming is a cornerstone of modern application development, enabling software to remain responsive and efficient even during long-running operations. In C#, the async and await keywords simplify asynchronous workflows, making them intuitive to implement. This section delves into the importance of asynchronous programming, explains its key components, and offers practical examples of its use.
Asynchronous programming is essential for applications that perform time-intensive tasks, such as network requests, file I/O, or database operations, without blocking the main thread. By freeing up resources during such operations, asynchronous programming enhances responsiveness and improves performance, particularly in user-facing applications.
public async Task<string> FetchDataAsync(string url)
{
HttpClient client = new HttpClient();
string data = await client.GetStringAsync(url); // Non-blocking
return data;
}
Asynchronous programming often involves Task objects, which represent operations that execute independently of the main thread. To understand their advantages, it’s helpful to compare them to traditional threads:
Example:
Task<int> task = Task.Run(() => Compute());
int result = await task; // Waits for the task to complete
Example:
Thread thread = new Thread(() => Compute());
thread.Start();
thread.Join(); // Blocks until the thread completes
For most scenarios, tasks are preferable due to their integration with async and await and their ability to handle asynchronous workflows efficiently.
public async Task<int> FetchDataAsync()
{
HttpClient client = new HttpClient();
string data = await client.GetStringAsync(“https://example.com”);
return data.Length;
}
Here, the method fetches data from a URL asynchronously, ensuring that the calling thread remains unblocked.
var tasks = new[] { Task1(), Task2(), Task3() };
await Task.WhenAll(tasks);
var firstCompletedTask = await Task.WhenAny(Task1(), Task2(), Task3());
try
{
await DoSomethingAsync();
}
catch (Exception ex)
{
Console.WriteLine($”Error: {ex.Message}”);
}
try
{
await Task.WhenAll(Task1(), Task2());
}
catch (AggregateException ex)
{
foreach (var innerException in ex.InnerExceptions)
{
Console.WriteLine($”Error: {innerException.Message}”);
}
}
Bad:
int result = task.Result; // Risk of deadlock
Good:
int result = await task;
await task.ConfigureAwait(false);
// Async all the way:
public async Task ProcessDataAsync()
{
var data = await FetchDataAsync();
await SaveDataAsync(data);
}
public async Task SaveDataAsync() { … } // Preferred
var semaphore = new SemaphoreSlim(3);
await semaphore.WaitAsync();
try
{
await PerformTaskAsync();
}
finally
{
semaphore.Release();
}