Implement data access

  • 10/11/2018

Skill 4.3: Query and manipulate data and objects by using LINQ

In the previous section you saw how a program can work with stored data by using SQL commands. SQL commands are time consuming to create and processing the result of a query is hard work. Language INtegrated Query, or LINQ, was created to make it very easy for C# programmers to work with data sources. This section covers LINQ and how to use it.

Note that there are a lot of example programs in this section, and the LINQ statements might be a bit hard to understand at first. However, remember that you can download and run all of the example code.

Sample application

In order to provide a good context for the exploration of LINQ we are going to develop the MusicTracks application used earlier in this book. This application allows the storage of music track data. Figure 4-15 shows the classes in the system. Note that at the moment we are not using a database to store the class information. Later we will consider how to map this design onto a database.

In previous versions of the application, the MusicTrack class contained a string that gave the name of the artist that recorded the track. In the new design, a MusicTrack contains a reference to an Artist object that describes the artist that recorded the track. Figure 4-15 shows how this works. If an artist records more than one track (which is very likely), the artist details will only be stored once and referred to by many MusicTrack instances.

FIGURE 4-15

FIGURE 4-15 Music tracks class design

The code next shows the C# code for the classes.

class Artist
{
    public string Name { get; set; }
}
class MusicTrack
{
    public Artist Artist { get; set; }
    public string Title { get; set; }
    public int Length { get; set; }
}

Now that you have the class design for the application, the next thing to do is create some sample data. This must be done programmatically. Testing a system by entering data by hand is a bad idea for a number of reasons. First, it is very time-consuming. Second, any changes to the data store design will mean that you will probably have to enter all the data again. And third, the act of creating the test data can give useful insights into your class design.

Listing 4-29 shows code that creates some artists and tracks. You can increase the amount of test data by adding more artists and titles. In this version all of the artists recorded all of the tracks. A random number generator provides each track a random length in the range of 20 to 600 seconds. Note that because the random number generator has a fixed seed value the lengths of each track will be the same each time you run the program.

LISTING 4-29 Musictrack classes

string[] artistNames = new string[] { "Rob Miles", "Fred Bloggs",
                                      "The Bloggs Singers", "Immy Brown" };
string[] titleNames = new string[] { "My Way", "Your Way", "His Way", "Her Way",
                                     "Milky Way" };

List<Artist> artists = new List<Artist>();
List<MusicTrack> musicTracks = new List<MusicTrack>();

Random rand = new Random(1);

foreach (string artistName in artistNames)
{
    Artist newArtist = new Artist { Name = artistName };
    artists.Add(newArtist);
    foreach (string titleName in titleNames)
    {
        MusicTrack newTrack = new MusicTrack
        {
            Artist = newArtist,
            Title = titleName,
            Length = rand.Next(20, 600)
        };
        musicTracks.Add(newTrack);
    }
}

foreach (MusicTrack track in musicTracks)
{
    Console.WriteLine("Artist:{0} Title:{1} Length:{2}", 
        track.Artist.Name, track.Title, track.Length);
}

Use an object initializer

If you look at the code in Listing 4-29 you will see that we are using object initializer syntax to create new instances of the music objects and initialize their values at the same time. This is a very useful C# feature that allows you to initialize objects when they are created without the need to create a constructor method in the class being initialized.

The code next shows how it works. The statement creates and initializes a new MusicTrack instance. Note the use of braces ({and}) to delimit the items that initialize the instance and commas to separate each value being used to initialize the object.

MusicTrack newTrack = new MusicTrack
{
    Artist = "Rob Miles",
    Title = "My Way",
    Length = 150
};

You don’t have to initialize all of the elements of the instance; any properties not initialized are set to their default values (zero for a numeric value and null for a string). The properties to be initialized in this way must all be public members of the class.

Use a LINQ operator

Now that you have some data can use LINQ operators to build queries and extract results from the data. The code in Listing 4-30 prints out the titles of all the tracks that were recorded by the artist with the name “Rob Miles.” The first statement uses a LINQ query to create an enumerable collection of MusicTrack references called selectedTracks that is then enumerated by the foreach construction to print out the results.

LISTING 4-30 LINQ operators

IEnumerable<MusicTrack> selectedTracks = from track in musicTracks where track.Artist.Name == "Rob Miles" select track;

foreach (MusicTrack track in selectedTracks)
{
    Console.WriteLine("Artist:{0} Title:{1}", track.Artist.Name, track.Title);
}

The LINQ query returns an IEnumerable result that is enumerated by a foreach construction. You can find an explanation of IEnumerable in Skill 2.4 in the “IEnumerable” section. The “Create method-based LINQ queries” section has more detail on how a query is actually implemented by C# code.

Use the var keyword with LINQ

The C# language is “statically typed.” The type of objects in a program is determined at compile time and the compiler rejects any actions that are not valid. For example, the following code fails to compile because the compiler will not allow a string to be subtracted from a number.

string name = "Rob Miles";
int age = 21;
int silly = age  - name;

This provides more confidence that our programs are correct before they run. The downside is that you have to put in the effort of giving each variable a type when you declare it. Most of the time, however, the compiler can infer the type to be used for any given variable. The name variable in the previous example must be of type string, since a string is being assigned to it. By the same logic, the age variable must be an int.

You can simplify code by using the var keyword to tell the compiler to infer the type of the variable being created from the context in which the variable is used. The compiler will define a string variable called namev in response to the following statement:

var namev = "Rob Miles";

Note. that this does not mean that the compiler cannot detect compilation errors. The statements in Listing 4-31 still fails to compile:

LISTING 4-31 var errors

var namev = "Rob Miles";
var agev = 21;
var sillyv = agev - namev;

The var keyword is especially useful when using LINQ. The result of a simple LINQ query is an enumerable collection of the type of data element held in the data source. The statement next shows the query from Listing 4-30.

IEnumerable<MusicTrack> selectedTracks = 
    from track in musicTracks where track.Artist.Name == "Rob Miles" select track;

To write this statement you must find out the type of data in the musicTracks collection, and then use this type with IEnumerable. The var keyword makes this code much easier to write (see Listing 4-32).

LISTING 4-32 var and LINQ

var selectedTracks =
    from track in musicTracks where track.Artist.Name == "Rob Miles" select track;

There are some situations where you won’t know the type of a variable when writing the code. Later in this section you will discover objects that are created dynamically as the program runs and have no type at all. These are called anonymous types. The only way code can refer to these is by use of variables of type var.

You can use the var type everywhere in your code if you like, but please be careful. A statement such as the following will not make you very popular with fellow programmers because it is impossible for them to infer the type of variable v without digging into the code and finding out what type is returned by the DoRead method.

var v = DoRead();

If you really want to use var in these situations you should make sure that you select variable names that are suitably informative, or you can infer the type of the item from the code, as in the following statements.

var nextPerson = DoReadPerson();
var newPerson = new Person();

LINQ projection

You can use the select operation in LINQ to produce a filtered version of a data source. In previous examples you discovered all of the tracks recorded by a particular artist. You can create other search criteria, for example by selecting the tracks with a certain title, or tracks longer than a certain length.

The result of a select is a collection of references to objects in the source data collection. There are a couple of reasons why a program might not want to work like this. First, you might not want to provide references to the actual data objects in the data source. Second, you might want the result of a query to contain a subset of the original data.

You can use projection to ask a query to “project” the data in the class onto new instances of a class created just to hold the data returned by the query. Let’s start by creating the class called TrackDetails that will hold just the artist name and the title of a track. You will use this to hold the result of the search query.

class TrackDetails
{
    public string ArtistName;
    public string Title;
}

The query can now be asked to create a new instance of this class to hold the result of each query. Listing 4-33 shows how this works.

LISTING 4-33 LINQ projection

var selectedTracks = from track in musicTracks
                    where track.Artist.Name == "Rob Miles"
                    select new TrackDetails
                    {
                        ArtistName = track.Artist.Name,
                        Title = track.Title
                    };

Projection results like this are particularly useful when you are using data binding to display the results to the user. Values in the query result can be bound to items to be displayed.

Anonymous types

You can remove the need to create a class to hold the result of a search query by making the query return results of an anonymous type. You can see how this works in Listing 4-34. Note that the name of the type is now missing from the end of the select new statement.

LISTING 4-34 Anonymous type

var selectedTracks = from track in musicTracks
                        where track.Artist.Name == "Rob Miles"
                        select new // projection type name missing from here
                        {
                            ArtistName = track.Artist.Name,
                            track.Title
                        };

The query in Listing 4-34 creates new instances of an anonymous type that contain just the data items needed from the query. Instances of the new type are initialized using the object initializer syntax. In this case the first property in the type is the name of the artist recording the track, and the second is the title of the track. For the first property you actually supply the name of the field to be created in the new type. For the second property the property is created with same name as the source property, in this case the property name will be Title.

The item that is returned by this query is an enumerable collection of instances of a type that has no name. It is an anonymous type. This means you have to use a var reference to refer to the query result. You can iterate through the collection in this result as you would any other. Note that each item in the selectedTracks collection must now be referred to using var because its type has no name. The code next shows how var is used for each item when printing out the results of the query in Listing 4-34.

foreach (var item in selectedTracks)
{
    Console.WriteLine("Artist:{0} Title:{1}", item.ArtistName,item.Title);
}

Note that the use of an anonymous type doesn’t mean that the compiler is any less rigorous when checking the correctness of the code. If the program tries to use a property that is not present in the item, for example if it tries to obtain the Length property from the result of the query, this generates an error at compile time.

LINQ join

The class design used up to this point uses C# references to implement the associations between the objects in the system. In other words, a MusicTrack object contains a reference to the Artist object that represents the artist that recorded that track. If you store your data in a database however, you will not be able to store the associations in this way.

Instead, each item in the database will have a unique id (its primary key) and objects referring to this object will contain this ID value (a foreign key). Rather than a reference to an Artist instance, the MusicTrack now contains an ArtistID field that identifies the artist associated with that track. Figure 4-16 shows how this association is implemented.

FIGURE 4-16

FIGURE 4-16 Music tracks and Artist ID

This design makes it slightly more difficult to search for tracks by a particular artist. The program needs to find the ID value for the artist with the name being searched for and then search for any tracks with that value of artist id. Fortunately, LINQ provides a join operator that can be used to join the output of one LINQ query to the input of another.

Listing 4-35 shows how this works. The first query selects the artist with the name “Rob Miles.” The results of that query are joined to the second query that searches the musicTracks collection for tracks with an ArtistID property that matches that of the artist found by the first query.

LISTING 4-35 LINQ join

var artistTracks = from artist in artists where artist.Name == "Rob Miles"
                    join track in musicTracks on artist.ID equals track.ArtistID
                    select new
{
    ArtistName = artist.Name,
    track.Title
};

LINQ group

Another useful LINQ feature is the ability to group the results of a query to create a summary output. For example, you may want to create a query to tell how many tracks there are by each artist in the music collection.

Listing 4-36 shows how to do this. The group action is given the data item to group by and the property by which it is to be grouped. The artistTrackSummary contains an entry for each different artist. Each of the items in the summary has a Key property, which is the value that item is “grouped” around. You want to create a group around artists, so the key is the ArtistID value of each track. The Key property of the artistTrackSummary gives the value of this key. You can use behaviors provided by a summary object to find out about the contents of the summary, and the Count method returns the number of items in the summary. You will discover more summary commands in the discussion about the aggregate commands later in this section.

LISTING 4-36 LINQ group

var artistSummary = from track in musicTracks
                    group track by track.ArtistID
                    into artistTrackSummary
                    select new
                    {
                        ID = artistTrackSummary.Key,
                        Count = artistTrackSummary.Count()
                    };

You can print out the contents of the anonymous classes produced by this query by using a foreach loop as shown next.

foreach (var item in artistSummary)
{
    Console.WriteLine("Artist:{0} Tracks recorded:{1}",
        item.ID, item.Count);
}

The problem with this query is that when run it produces the results as shown next. Rather than generating the name of the artist, the program displays the ArtistID values.

Artist:0 Tracks recorded:5
Artist:6 Tracks recorded:5
Artist:12 Tracks recorded:5
Artist:18 Tracks recorded:5

You can fix this by making use of a join operation that will extract the artist name for use in the query. The needed joinis shown next. You can then create the group keyed on the artist name rather than the ID to get the desired output.

var artistSummaryName = from track in musicTracks
                join artist in artists on track.ArtistID equals artist.ID
                group track by artist.Name
                into artistTrackSummary
                select new
                {
                    ID = artistTrackSummary.Key,
                    Count = artistTrackSummary.Count()
                };

The output from this query is shown here:

Artist:Rob Miles Tracks recorded:5
Artist:Fred Bloggs Tracks recorded:5
Artist:The Bloggs Singers Tracks recorded:5
Artist:Immy Brown Tracks recorded:5

Note that this is strong LINQ magic. It is worth playing with the sample code a little and examining the structure of the query to see what is going on.

LINQ Take and skip

A LINQ query will normally return all of the items that if finds. However, this might be more items that your program wants. For example, you might want to show the user the output one page at a time. You can use take to tell the query to take a particular number of items and the skip to tell a query to skip a particular number of items in the result before taking the requested number.

The sample program in Listing 4-37 displays all of the music tracks ten items at a time. It uses a loop that uses Skip to move progressively further down the database each time the loop is repeated. The loop ends when the LINQ query returns an empty collection. The user presses a key at the end of each page to move onto the next page.

LISTING 4-37 LINQ take and skip

int pageNo = 0;
int pageSize = 10;

while(true)
{
    // Get the track information
    var trackList = from musicTrack in musicTracks.Skip(pageNo*pageSize).Take(pageSize)
                    join artist in artists on musicTrack.ArtistID equals artist.ID
                    select new
                    {
                        ArtistName = artist.Name,
                        musicTrack.Title
                    };

    // Quit if we reached the end of the data
    if (trackList.Count() == 0)
        break;

    // Display the query result
    foreach (var item in trackList)
    {
        Console.WriteLine("Artist:{0} Title:{1}",
            item.ArtistName, item.Title);
    }

    Console.Write("Press any key to continue");
    Console.ReadKey();

    // move on to the next page
    pageNo++;
}

LINQ aggregate commands

In the context of LINQ commands, the word aggregate means “bring together a number of related values to create a single result.” You can use aggregate operators on the results produced by group operations. You have already used one aggregate operator in a LINQ query. You used the Count operator in Listing 4-36 to count the number of tracks in a group extracted by artist. That provided the number of tracks assigned to a particular artist. You may want to get the total length of all the tracks assigned to an artist, and for that you can use the Sum aggregate operator.

The parameter to the Sum operator is a lambda expression that the operator will use on each item in the group to obtain the value to be added to the total sum for that item. To get the sum of MusicTrack lengths, the lambda expression just returns the value of the Length property for the item. Listing 4-38 shows how this works.

LISTING 4-38 LINQ aggregate

var artistSummary = from track in musicTracks
           join artist in artists on track.ArtistID equals artist.ID
           group track by artist.Name
           into artistTrackSummary
           select new
           {
                   ID = artistTrackSummary.Key,
                   Length = artistTrackSummary.Sum(x => x.Length)
           };

The result of this query is a collection of anonymous objects that contain the name of the artist and the total length of all the tracks recorded by that artist. The program produces the following output:

Name:Rob Miles  Total length:1406
Name:Fred Bloggs  Total length:1533
Name:The Bloggs Singers  Total length:1413
Name:Immy Brown  Total length:1813

You can use Average, Max, and Min to generate other items of aggregate information. You can also create your own Aggregate behavior that will be called on each successive item in the group and will generate a single aggregate result.

Create method-based LINQ queries

The first LINQ query that you saw was in Listing 4-30 as shown here.

IEnumerable<MusicTrack> selectedTracks = 
     from track in musicTracks where track.Artist.Name == "Rob Miles" select track;

The query statement uses query comprehension syntax, which includes the operators from, in, where, and select. The compiler uses these to generate a call to the Where method on the MusicTracks collection. In other words, the code that is actually created to perform the query is the statement below:

IEnumerable<MusicTrack> selectedTracks =
        musicTracks.Where(track => track.Artist.Name == "Rob Miles");

The Where method accepts a lambda expression as a parameter. In this case the lambda expression accepts a MusicTrack as a parameter and returns True if the Name property of the Artist element in the MusicTrack matches the name that is being selected.

You first saw lambda expressions in Skill 1.4, “Create and implement events and callbacks.” A lambda expression is a piece of behavior that can be regarded as an object. In this situation the Where method is receiving a piece of behavior that the method can use to determine which tracks to select. In this case the behavior is “take a track and see if the artist name is Rob Miles.” You can create your own method-based queries instead of using the LINQ operators. Listing 4-39 shows the LINQ query and the matching method-based behavior.

LISTING 4-39 Method based query

//IEnumerable<MusicTrack> selectedTracks = from track in musicTracks where
      track.Artist.Name == "Rob Miles" select track;
// Method based implementation of this query
IEnumerable<MusicTrack> selectedTracks = musicTracks.Where(track =>
      track.Artist.Name == "Rob Miles");

Programs can use the LINQ query methods (and execute LINQ queries) on data collections, such as lists and arrays, and also on database connections. The methods that implement LINQ query behaviors are not added to the classes that use them. Instead they are implemented as extension methods. You can find out more about extension methods in Skill 2.1, in the “Extension methods” section.

Query data by using query comprehension syntax

The phrase “query comprehension syntax” refers to the way that you can build LINQ queries for using the C# operators provided specifically for expressing data queries. The intention is to make the C# statements that strongly resemble the SQL queries that perform the same function. This makes it easier for developers familiar with SQL syntax to use LINQ.

Listing 4-40 shows a complex LINQ query that is based on the LINQ query used in Listing 4-38 to produce a summary giving the length of music by each artist. This uses the orderby operator to order the output by artist name.

LISTING 4-40 Complex query

var artistSummary = from track in musicTracks
                      join artist in artists on track.ArtistID equals artist.ID
                      group track by artist.Name
                      into artistTrackSummary
                      select new
                      {
                               ID = artistTrackSummary.Key,
                                Length = artistTrackSummary.Sum(x => x.Length)
                      };

The SQL query that matches this LINQ is shown below:

SELECT SUM([t0].[Length]) AS [Length], [t1].[Name] AS [ID]
FROM [MusicTrack] AS [t0]
INNER JOIN [Artist] AS [t1] ON [t0].[ArtistID] = [t1].[ID]
GROUP BY [t1].[Name]

This output was generated using the LINQPad application that allows programmers to create LINQ queries and view the SQL and method-based implementations. The standard edition is a very powerful resource for developers and can be downloaded for free from http://www.linqpad.net/.

Select data by using anonymous types

You first saw the use of anonymous types in the “Anonymous types” section earlier in this chapter. The last few sample programs have shown the use of anonymous types moving from creating values that summarize the contents of a source data object (for example extracting just the artist and title information from a MusicTrack value), to creating completely new types that contain data from the database and the results of aggregate operators.

It is important to note that you can also create anonymous type instances in method-based SQL queries. Listing 4-41 shows the method-based implementation of the query from Listing 4-40; anonymous type is shown in bold. Note the use of an intermediate anonymous class that is used to implement the join between the two queries and generate objects that contain artist and track information.

LISTING 4-41 Complex anonymous types

var artistSummary = MusicTracks.Join(
                        Artists,
                        track => track.ArtistID,
                        artist => artist.ID,
                        (track, artist) =>
                            new
                            {
                                track = track,
                                artist = artist
                            }
                    )
                    .GroupBy(
                        temp0 => temp0.artist.Name,
                        temp0 => temp0.track
                    )
                    .Select(
                        artistTrackSummary =>
                            new
                            {
                                ID = artistTrackSummary.Key,
                                Length = artistTrackSummary.Sum(x => x.Length)
                            }
                    );

Force execution of a query

The result of a LINQ query is an item that can be iterated. We have used the foreach construction to display the results from queries. The actual evaluation of a LINQ query normally only takes place when a program starts to extract results from the query. This is called deferred execution. If you want to force the execution of a query you can use the ToArray() method as shown in Listing 4-42. The query is performed and the result returned as an array.

LISTING 4-42 Force query execution

var artistTracksQuery = from artist in artists
                    where artist.Name == "Rob Miles"
                    join track in musicTracks on artist.ID equals track.ArtistID
                    select new
                    {
                        ArtistName = artist.Name,
                        track.Title
                    };

var artistTrackResult = artistTracksQuery.ToArray();

foreach (var item in artistTrackResult)
{
    Console.WriteLine("Artist:{0} Title:{1}",
        item.ArtistName, item.Title);
}

Note that in the case of this example the result will be an array of anonymous class instances. Figure 4-17 shows the view in Visual Studio of the contents of the result. The program has been paused just after the artistTrackResult variable has been set to the query result, and the debugger is showing the contents of the artistTrackResult.

FIGURE 4-17

FIGURE 4-17 Immediate query results

A query result also provides ToList and ToDictionary methods that will force the execution of the query and generate an immediate result of that type. If a query returns a singleton value (for example the result of an aggregation operation such as sum or count) it will be immediately evaluated.

Read, filter, create, and modify data structures by using LINQ to XML

In this section we are going to investigate the LINQ to XML features that allow you to use LINQ constructions to parse XML documents. The classes that provide these behaviors are in the System.XML.Linq namespace.

Sample XML document

The sample XML document is shown next. It contains two MusicTrack items that are held inside a MusicTracks element. The text of the sample document is stored in a string variable called XMLText.

string XMLText =
    "<MusicTracks> " +
        "<MusicTrack> " +
            "<Artist>Rob Miles</Artist>  " +
            "<Title>My Way</Title>  " +
            "<Length>150</Length>" +
        "</MusicTrack>" +

        "<MusicTrack>" +
            "<Artist>Immy Brown</Artist>  " +
            "<Title>Her Way</Title>  " +
            "<Length>200</Length>" +
        "</MusicTrack>" +
    "</MusicTracks>";

Read XML with LINQ to XML and XDocument

In the previous section, “Consume XML data” you learned how to consume XML data in a program using the XMLDocument class. This class has been superseded in later versions of .NET (version 3.5 onwards) by the XDocument class, which allows the use of LINQ queries to parse XML files.

A program can create an XDocument instance that represents the earlier document by using the Parse method provided by the XDocument class as shown here.

XDocument musicTracksDocument = XDocument.Parse(XMLText);

The format of LINQ queries is slightly different when working with XML. This is because the source of the query is a filtered set of XML entries from the source document. Listing 4-43 shows how this works. The query selects all the “MusicTrack” elements from the source document. The result of the query is an enumeration of XElement items that have been extracted from the document. The XElement class is a development of the XMLElement class that includes XML behaviors. The program uses a foreach construction to work through the collection of XElement results, extracting the required text values.

LISTING 4-43 Read XML with LINQ

IEnumerable<XElement> selectedTracks =
    from track in musicTracksDocument.Descendants("MusicTrack") select track;
foreach (XElement item in selectedTracks)
{
    Console.WriteLine("Artist:{0} Title:{1}",
        item.Element("Artist").FirstNode,
        item.Element("Title").FirstNode);
}

Filter XML data with LINQ to XML

The program in Listing 4-43 displays the entire contents of the XML document. A program can perform filtering in the query by adding a where operator, just as with the LINQ we have seen before. Listing 4-44 shows how this works. Note that the where operation has to extract the data value from the element so that it can perform the comparison.

LISTING 4-44 Filter XML with LINQ

IEnumerable<XElement> selectedTracks =
       from track in musicTracksDocument.Descendants("MusicTrack")
             where (string) track.Element("Artist") == "Rob Miles"
             select track;

The LINQ queries that we have seen so far have been expressed using query comprehension. It is possible, however, to express the same query in the form of a method-based query. The Descendants method returns an object that provides the Where behavior. The code next shows the query in Listing 4-44 implemented as a method-based query.

IEnumerable<XElement> selectedTracks =
  musicTracksDocument.Descendants("MusicTrack").Where(element => 
(string)element.Element("Artist") == "Rob Miles");

Create XML with LINQ to XML

The LINQ to XML features include very easy way to create XML documents. The code in Listing 4-45 creates a document exactly like the sample XML for this section. Note that the arrangement of the constructor calls to each XElement item mirror the structure of the document.

LISTING 4-45 Create XML with LINQ

XElement MusicTracks = new XElement("MusicTracks",
    new List<XElement>
    {
        new XElement("MusicTrack",
            new XElement("Artist", "Rob Miles"),
            new XElement("Title", "My Way")),
        new XElement("MusicTrack",
            new XElement("Artist", "Immy Brown"),
            new XElement("Title", "Her Way"))
    }
 );

Modify data with LINQ to XML

The XElement class provides methods that can be used to modify the contents of a given XML element. The program in Listing 4-46 creates a query that identifies all the MusicTrack items that have the title “my Way” and then uses the ReplaceWith method on the title data in the element to change the title to the correct title, which is “My Way.”

LISTING 4-46 Modify XML with LINQ

IEnumerable<XElement> selectedTracks =
    from track in musicTracksDocument.Descendants("MusicTrack")
        where (string) track.Element("Title") == "my Way"
        select track;

foreach (XElement item in selectedTracks)
{
    item.Element("Title").FirstNode.ReplaceWith("My Way");
}

As you saw when creating a new XML document, an XElement can contain a collection of other elements to build the tree structure of an XML document. You can programmatically add and remove elements to change the structure of the XML document.

Suppose that you decide to add a new data element to MusicTrack. You want to store the “style” of the music, whether it is “Pop”, “Rock,” or “Classical.” The code in Listing 4-47 finds all of the items in our sample data that are missing a Style element and then adds the element to the item.

LISTING 4-47 Add XML with LINQ

IEnumerable<XElement> selectedTracks =
    from track in musicTracksDocument.Descendants("MusicTrack")
        where track.Element("Style") == null
        select track;

foreach (XElement item in selectedTracks)
{
    item.Add(new XElement("Style", "Pop"));
}