Skip to main content

.NET LINQ Projection

Introduction

Projection is one of the most powerful features in LINQ (Language Integrated Query). When we talk about projection in LINQ, we're referring to the process of transforming data from one form to another. It's similar to how a movie projector takes a film and projects it onto a screen in a different format.

In LINQ, projection allows you to:

  • Extract specific properties from objects
  • Transform data into entirely new types
  • Reshape your data structures to fit your needs
  • Create anonymous types on the fly

Projection operations in LINQ primarily use the Select and SelectMany methods. These methods allow you to define exactly how you want your output data to be structured, regardless of the input structure.

The Select Method

The Select method is the basic projection operator in LINQ. It transforms each element from the source sequence into a new form.

Basic Syntax

csharp
var result = collection.Select(item => expression);

Where:

  • collection is your source data
  • item represents each element in the collection
  • expression defines how to transform each element

Simple Select Examples

Let's start with a basic example using a list of numbers:

csharp
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// Project each number into its squared value
var squaredNumbers = numbers.Select(n => n * n);

// Display results
Console.WriteLine("Original numbers:");
foreach (var num in numbers)
Console.WriteLine(num);

Console.WriteLine("\nSquared numbers:");
foreach (var num in squaredNumbers)
Console.WriteLine(num);

Output:

Original numbers:
1
2
3
4
5

Squared numbers:
1
4
9
16
25

Projecting Object Properties

Consider a more realistic scenario with a class:

csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}

// Create a list of products
List<Product> products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 1200.00m, Category = "Electronics" },
new Product { Id = 2, Name = "Desk Chair", Price = 250.50m, Category = "Furniture" },
new Product { Id = 3, Name = "Coffee Maker", Price = 89.99m, Category = "Kitchen" },
new Product { Id = 4, Name = "Monitor", Price = 350.75m, Category = "Electronics" }
};

// Project to get only the names
var productNames = products.Select(p => p.Name);

Console.WriteLine("Product Names:");
foreach (var name in productNames)
Console.WriteLine(name);

Output:

Product Names:
Laptop
Desk Chair
Coffee Maker
Monitor

Creating Anonymous Types

LINQ projection allows us to create new anonymous types on the fly:

csharp
// Project to a new anonymous type with selected properties
var productInfo = products.Select(p => new {
ProductName = p.Name,
ProductPrice = p.Price
});

Console.WriteLine("\nProduct Information:");
foreach (var item in productInfo)
Console.WriteLine($"{item.ProductName}: ${item.ProductPrice}");

Output:

Product Information:
Laptop: $1200.00
Desk Chair: $250.50
Coffee Maker: $89.99
Monitor: $350.75

Using Index in Select

Sometimes you might need to know the position of each element in the collection:

csharp
// Using the index in the projection
var numberedProducts = products.Select((p, index) =>
new { Number = index + 1, Name = p.Name });

Console.WriteLine("\nNumbered Products:");
foreach (var item in numberedProducts)
Console.WriteLine($"{item.Number}. {item.Name}");

Output:

Numbered Products:
1. Laptop
2. Desk Chair
3. Coffee Maker
4. Monitor

The SelectMany Method

While Select produces one output element for each input element, SelectMany allows you to flatten nested collections into a single collection. It's extremely useful when working with hierarchical or nested data.

Basic Syntax

csharp
var result = collection.SelectMany(item => item.NestedCollection);

SelectMany Examples

Let's look at a scenario with nested collections:

csharp
public class Student
{
public string Name { get; set; }
public List<string> Courses { get; set; }
}

// Create a list of students
List<Student> students = new List<Student>
{
new Student {
Name = "Alice",
Courses = new List<string> { "Math", "Physics", "Chemistry" }
},
new Student {
Name = "Bob",
Courses = new List<string> { "Biology", "English", "Art" }
},
new Student {
Name = "Charlie",
Courses = new List<string> { "Computer Science", "Statistics" }
}
};

// Using Select (creates nested lists)
var nestedCourses = students.Select(s => s.Courses);
// This produces IEnumerable<List<string>> - a collection of lists

// Using SelectMany (flattens to a single list)
var allCourses = students.SelectMany(s => s.Courses);
// This produces IEnumerable<string> - a flattened collection of strings

Console.WriteLine("All courses (flattened with SelectMany):");
foreach (var course in allCourses)
Console.WriteLine(course);

Output:

All courses (flattened with SelectMany):
Math
Physics
Chemistry
Biology
English
Art
Computer Science
Statistics

Combining Elements with SelectMany

We can also use SelectMany to combine elements from the outer and inner collections:

csharp
// Combining student names with their courses
var studentCourses = students.SelectMany(
s => s.Courses,
(student, course) => new { StudentName = student.Name, CourseName = course }
);

Console.WriteLine("\nStudent Course Enrollments:");
foreach (var sc in studentCourses)
Console.WriteLine($"{sc.StudentName} is taking {sc.CourseName}");

Output:

Student Course Enrollments:
Alice is taking Math
Alice is taking Physics
Alice is taking Chemistry
Bob is taking Biology
Bob is taking English
Bob is taking Art
Charlie is taking Computer Science
Charlie is taking Statistics

Real-World Applications

Let's explore how projection is used in real-world scenarios.

Example 1: Data Transfer Objects (DTOs)

In web applications, we often need to convert database entities to DTOs (Data Transfer Objects) before sending them to the client:

csharp
public class CustomerEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public DateTime DateOfBirth { get; set; }
public bool IsSubscribedToNewsletter { get; set; }
public string InternalNotes { get; set; }
}

public class CustomerDto
{
public int Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
}

// Simulate database query
List<CustomerEntity> customers = GetCustomersFromDatabase();

// Project to DTOs for API response
var customerDtos = customers.Select(c => new CustomerDto
{
Id = c.Id,
FullName = $"{c.FirstName} {c.LastName}",
Email = c.Email
});

Example 2: Data Analysis and Reporting

When generating reports, you often need to calculate aggregates or reshape data:

csharp
public class SalesRecord
{
public DateTime Date { get; set; }
public string Product { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}

// Simulate sales data
List<SalesRecord> sales = GetSalesData();

// Generate daily sales report
var dailySalesReport = sales
.GroupBy(s => s.Date.Date)
.Select(g => new {
Date = g.Key,
TotalSales = g.Sum(s => s.Quantity * s.UnitPrice),
ProductsSold = g.Count(),
TopProduct = g.OrderByDescending(s => s.Quantity).First().Product
});

// Display report
foreach (var day in dailySalesReport)
{
Console.WriteLine($"Date: {day.Date.ToShortDateString()}");
Console.WriteLine($"Total Sales: ${day.TotalSales}");
Console.WriteLine($"Products Sold: {day.ProductsSold}");
Console.WriteLine($"Top Selling Product: {day.TopProduct}");
Console.WriteLine();
}

Example 3: Working with JSON Data

When parsing JSON data, you often need to transform and extract specific information:

csharp
using System.Text.Json;

string jsonResponse = @"{
""results"": [
{
""id"": 1,
""name"": ""John"",
""details"": {
""age"": 32,
""location"": ""New York""
}
},
{
""id"": 2,
""name"": ""Sarah"",
""details"": {
""age"": 28,
""location"": ""Boston""
}
}
]
}";

using System.Text.Json.Nodes;
var jsonObj = JsonNode.Parse(jsonResponse);
var results = jsonObj["results"].AsArray();

// Project JSON data to strongly typed objects
var users = results.Select(r => new {
Id = (int)r["id"],
Name = (string)r["name"],
Age = (int)r["details"]["age"],
Location = (string)r["details"]["location"]
});

foreach (var user in users)
{
Console.WriteLine($"User: {user.Name}, Age: {user.Age}, Location: {user.Location}");
}

Output:

User: John, Age: 32, Location: New York
User: Sarah, Age: 28, Location: Boston

Performance Considerations

When using projection, keep in mind:

  1. Deferred Execution: LINQ projection operations use deferred execution, meaning they aren't executed until you iterate through the results.

  2. Memory Usage: Creating new objects for each element can consume memory. For large datasets, consider using streaming approaches.

  3. Select vs. SelectMany: Choose the right operator for your needs. SelectMany flattens collections but may be less efficient for simple projections.

  4. Avoid Multiple Enumerations: Store projected results in a collection (like a List<T>) if you need to iterate through them multiple times.

csharp
// This will enumerate twice
var projection = numbers.Select(n => ExpensiveCalculation(n));
foreach (var p in projection) { /* first use */ }
foreach (var p in projection) { /* second use - recalculates everything! */ }

// Better approach
var storedProjection = numbers.Select(n => ExpensiveCalculation(n)).ToList();
foreach (var p in storedProjection) { /* first use */ }
foreach (var p in storedProjection) { /* second use - uses cached results */ }

Summary

LINQ projection operations are essential tools in the .NET developer's toolkit. They allow you to:

  • Transform data from one form to another using Select
  • Flatten nested collections with SelectMany
  • Create new types on the fly, including anonymous types
  • Extract only the data you need, reducing memory usage

By mastering projection, you can write cleaner, more efficient code that shapes data exactly as needed for your application.

Additional Resources

Exercises

  1. Create a list of integers and use LINQ projection to generate the square root of each number.

  2. Given a list of words, project each word to a new object containing the word and its length.

  3. Given a list of Person objects with Name and Address properties (where Address is another object with City and Country properties), use projection to create a list of strings in the format "Name lives in City, Country".

  4. Create a nested list structure (like a list of lists) and use SelectMany to flatten it into a single list.

  5. Use projection to transform a list of dates into a list of strings showing the day of week (e.g., "Monday", "Tuesday").



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)