In this article, we’ll explore flattening nested collections in C#. We’ll examine different approaches for handling from simple to complex data structures, offering practical guidance to effectively flatten nested collections within our C# applications.

To download the source code for this article, you can visit our GitHub repository.

Let’s get started.

Importance of Flattening Nested Collections

Nested collections can introduce unnecessary complexity, making it challenging to work with the data. Flattening these collections can simplify the structure, making it easier to understand and manipulate.

Also, organizing related records into a single, cohesive structure can lead to more efficient data storage and processing.

Moreover, by restructuring nested collections into a more linear format, we represent the data in a tabular format, which might be necessary for input in a database or integration with external systems.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Use of Select() Method to Flatten Specific Property

We will use as an example the department structure along with employees as nested collections. Let’s inspect the Department record:

public record Department(string Name, IEnumerable<Employee> Employees);

We include only two properties, a string for its Name and the nested collection of Employee objects. The constructor defined within the record syntax we use is a primary constructor. In C#, a primary constructor is a concise way to define a constructor that initializes the properties of a record. Similarly, we design the Employee record:

public record Employee(string Name, string Email);

For simplicity, here we include only the employee’s name and an email. Next, we initialize a Department entity:

var department = new Department(
    "Human Resources",
    [
        new("Alice", "[email protected]"),
        new("Bob", "[email protected]" )
    ]
);

The Department record is the parent entity, while the Employee record is the child. One Department consists of one or more Employee entities. Next, we will create the entity we are expecting to receive as a result:

public record DepartmentFlattened(string DepartmentName, string EmployeeName, string EmployeeEmail);

This object includes all properties of every other object at the same level. Now, let’s build a simple method for flattening the parent Department object:

public static IEnumerable<DepartmentFlattened> FlattenWithSelect(Department department)
{
    return department?.Employees?.Select(employee => 
        new DepartmentFlattened(
            department.Name, 
            employee.Name, 
            employee.Email
            )
        );
}

Our method FlattenWithSelect() takes as input a Department object and returns a collection of DepartmentFlattened object. We use the LINQ syntax and the Select() method to iterate over the Employee object collection within the provided Department.

For each Employee, we construct a new DepartmentFlattened object that includes every property value such as the DepartmentName, EmployeeName, and EmployeeEmail. Consequently, we transform the entity into a flattened format by projecting these selected properties into a sequence of the objects we desire.

Finally, we can flatten our nested department variable with the FlattenWithSelect() method:

{ DepartmentName = Human Resources, EmployeeName = Alice, EmployeeEmail = [email protected] }
{ DepartmentName = Human Resources, EmployeeName = Bob, EmployeeEmail = [email protected] }

We can verify that the outcome includes every combination of the objects. We have a list of two Employee objects from one Department. So, the flattened objects are two in total, containing all the available properties.

Let’s see a different syntax now:

public static IEnumerable<DepartmentFlattened> FlattenWithQueryExpression(Department department)
{
    return from employee in department?.Employees ?? []
        select new DepartmentFlattened(department.Name, employee.Name, employee.Email);
}

This time, we use query expression, a SQL-like syntax for composing queries over collections. In brief, it allows us to express operations in a more readable way. 

As we expect, the method FlattenWithQueryExpression() method performs the same. 

Flatten Multiple Nested Collections With SelectMany()

Now, we will make the scenario a little bit more complex. We will add one more nested collection property to the Department object, called Project:

public record Project(string Title, int Budget);

The Project record consists of a string property name Title and an int property name Budget, for demonstration purposes. Also, we extend the result object:

public record DepartmentFlattenedMultipleCollections(
    string DepartmentName, 
    string EmployeeName, 
    string EmployeeEmail, 
    string ProjectTitle, 
    int ProjectBudget
);

Here, we add the string property ProjectTitle and int property ProjectBudget in order to include every combination. Now, we can build the new Department object:

var department = new Department(
    "Engineering",
    [
        new("Charlie", "[email protected]"), 
        new("David", "[email protected]")
    ],
    [
        new("Product Development", 50000), 
        new("Research", 20000)
    ]
);

We add two Project object and two Employee object instances in the Department entity. Next, we can define a new method to flatten the Department object:

public static IEnumerable<DepartmentFlattenedMultipleCollections>? FlattenWithSelectMany(Department department)
{
    return department?.Employees?.SelectMany(
        employee => department.Projects.Select(project => new DepartmentFlattenedMultipleCollections(
            department.Name,
            employee.Name, 
            employee.Email,
            project.Title,
            project.Budget
            )
        )
    );
}

In our FlattenWithSelectMany() method, we aim to simplify the complex structure of a Department object by using LINQ’s SelectMany() method. For every Employee in the Employees collection, we iterate over all Projects in the Projects collection. Consequently, this allows us to construct a new object for each combination of Employee and Project. Within each object, we include all the properties that are present.

When we apply to a collection of collections, the SelectMany() method projects each element of the outer collection to an inner collection. It is similar to a cross-join operation in SQL. 

Let’s see the outcome:

{ DepartmentName = Engineering, EmployeeName = Charlie, 
  EmployeeEmail = [email protected], ProjectTitle = Product Development, Budget = 50000 }
{ DepartmentName = Engineering, EmployeeName = Charlie, 
  EmployeeEmail = [email protected], ProjectTitle = Research, Budget = 20000 }
{ DepartmentName = Engineering, EmployeeName = David, 
  EmployeeEmail = [email protected], ProjectTitle = Product Development, Budget = 50000 }
{ DepartmentName = Engineering, EmployeeName = David, 
  EmployeeEmail = [email protected], ProjectTitle = Research, Budget = 20000 }

Now, we can see that we have all possible combinations between collection properties in the result. 

How to Flatten Two-Level Nested Collection

Now we will proceed with flattening an object that has a nested collection inside another one. Let’s enhance the Employee object with the Certification property:

public record Employee(
    string Name, 
    string Email, 
    IEnumerable<Certification>? Certifications = null
);

The Employee object includes a list of Certification objects. Likewise, our next modification is about the result object we want as the outcome:

public record DepartmentFlattenedComplex(
    string DepartmentName, 
    string EmployeeName, 
    string EmployeeEmail, 
    string CertificationTitle, 
    DateOnly CertificationIssueDate
);

In contrast to the previous similar object, we now have the string property CertificationTitle and the DateOnly property CertificationIssueDate. Respectively, we implement the method that handles this case:

public static IEnumerable<DepartmentFlattenedComplex>? FlattenComplexWithSelectMany(Department department)
{
    return department?.Employees?.SelectMany(
        employee => employee.Certifications?.Select(certification => new DepartmentFlattenedComplex(
            department.Name,
            employee.Name,
            employee.Email,
            certification.Title,
            certification.IssueDate
            )
        )
    );
}

Initially, we navigate through the Employee object collection within the provided Department. For each Employee, we utilize the SelectMany() method to go through the Certification collection of that Employee. Lastly, within the inner Select() method, we project each Certification into the final object containing all properties to create a flat structure.

Now let’s examine the result:

{ DepartmentName = Engineering, EmployeeName = Charlie, 
  EmployeeEmail = [email protected], Title = MCSD, IssueDate = 4/15/2020 }
{ DepartmentName = Engineering, EmployeeName = Charlie, 
  EmployeeEmail = [email protected], Title = PMP, IssueDate = 3/1/2022 }
{ DepartmentName = Engineering, EmployeeName = David, 
  EmployeeEmail = [email protected], Title = Solutions Architect, IssueDate = 7/5/2017 }
{ DepartmentName = Engineering, EmployeeName = David, 
  EmployeeEmail = [email protected], Title = CCNA, IssueDate = 10/1/2019 }

We now project every Employee and Certification at the level of the parent Department object. 

To summarize, we can effectively flatten any nested collection hierarchy by utilizing multiple SelectMany() operations. With each SelectMany() call, we iterate over a nested collection within the parent collection, transforming it into a flat structure. Thus, by chaining multiple SelectMany() calls, each targeting a different nested collection level, we progressively flatten the entire hierarchy into a single sequence of desired elements. 

It is worth mentioning that apart from LINQ usage, we can also accomplish object flattening using the AutoMapper (or similar) libraries. We can set up rules that instruct the AutoMapper mechanism how to convert from a complex, nested object and place it into a simpler, flat object.

Conclusion

In this article, we showcase solutions for flattening nested collections in objects. We examined from simple to complex cases, also demonstrating different syntax approaches.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!