Naming anonymous types

02 December 2009

I've been futzing around recently trying to get a grip on LINQ to XML for an internal project. Yeah, I know, everyone else has moved on having solved that particular problem a couple of years ago, but for some reason, although I understood the concepts and the infrastructure behind LINQ, I'd never really coded anything. Until this afternoon, that is.

Not one to take little nibbles, I decided to attack iTunes Music Library.xml.

If you've never taken a peek at this file, here's the header plus the top two tracks from mine:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Major Version</key><integer>1</integer>
    <key>Minor Version</key><integer>1</integer>
    <key>Application Version</key><string>9.0.2</string>
    <key>Features</key><integer>5</integer>
    <key>Show Content Ratings</key><true/>
    <key>Music Folder</key><string>file://localhost/O:/My%20Music/iTunes/iTunes%20Music/</string>
    <key>Library Persistent ID</key><string>277AA4870A436D01</string>
    <key>Tracks</key>
    <dict>
      <key>1656</key>
      <dict>
        <key>Track ID</key><integer>1656</integer>
        <key>Name</key><string>Voices</string>
        <key>Artist</key><string>Vangelis</string>
        <key>Album Artist</key><string>Vangelis</string>
        <key>Composer</key><string>Vangelis</string>key>Album</key><string>Voices</string> 
        <key>Genre</key><string>Electronica</string>
        <key>Kind</key><string>MPEG audio file</string>
        <key>Size</key><integer>5064895</integer>
        <key>Total Time</key><integer>421381</integer>
        <key>Track Number</key><integer>1</integer>
        <key>Year</key><integer>1995</integer>
        <key>Date Modified</key><date>2008-02-09T21:05:38Z</date>
        <key>Date Added</key><date>2005-05-06T23:21:31Z</date>
        <key>Bit Rate</key><integer>96</integer>
        <key>Sample Rate</key><integer>44100</integer>
        <key>Play Count</key><integer>10</integer> 
        <key>Play Date</key><integer>3327338137</integer>
        <key>Play Date UTC</key><date>2009-06-09T03:35:37Z</date>
        <key>Artwork Count</key><integer>1</integer>
        <key>Persistent ID</key><string>277AA4870A436D0E</string>
        <key>Track Type</key><string>File</string>
        <key>Location</key><string>file://localhost/M:/My%20Music/Vangelis/Voices/01%20-%20Voices.mp3</string>
        <key>File Folder Count</key><integer>-1</integer>
        <key>Library Folder Count</key><integer>-1</integer>
      </dict>
      <key>1658</key>
      <dict>
        <key>Track ID</key><integer>1658</integer>
        <key>Name</key><string>Echoes</string>
        <key>Artist</key><string>Vangelis</string>
        <key>Album Artist</key><string>Vangelis</string>
        <key>Composer</key><string>Vangelis</string>
        <key>Album</key><string>Voices</string>
        <key>Genre</key><string>Electronica</string>
        <key>Kind</key><string>MPEG audio file</string>
        <key>Size</key><integer>6067997</integer>
        <key>Total Time</key><integer>504973</integer>
        <key>Track Number</key><integer>2</integer>
        <key>Year</key><integer>1995</integer>
        <key>Date Modified</key><date>2008-02-09T21:05:38Z</date>
        <key>Date Added</key><date>2005-05-06T23:21:31Z</date>
        <key>Bit Rate</key><integer>96</integer>
        <key>Sample Rate</key><integer>44100</integer>
        <key>Play Count</key><integer>9</integer>
        <key>Play Date</key><integer>3330797581</integer>
        <key>Play Date UTC</key><date>2009-07-19T04:33:01Z</date>
        <key>Artwork Count</key><integer>1</integer>
        <key>Persistent ID</key><string>277AA4870A436D0F</string>
        <key>Track Type</key><string>File</string>
        <key>Location</key><string>file://localhost/M:/My%20Music/Vangelis/Voices/02%20-%20Echoes.mp3</string>
        <key>File Folder Count</key><integer>-1</integer>
        <key>Library Folder Count</key><integer>-1</integer>
      </dict>

Yep, it's a mess. The design is not brilliant: I was expecting to see major elements like "song", and inner elements called "Album" and "Artist" and the like. But no: it's a dictionary with every entry in the dictionary defined as a key-value pair or as another dictionary entry. Bleugh.

I almost tossed the idea, but then decided to do a quick search to see if anyone had figured out what to do. I came across this article by Joshua Allen in his Better Living Through Software blog. In it he described a method using LINQ that transformed the iTunes file into what I might call more normal-looking XML. So I took that idea and ran with it. Essentially I wanted a method that would return a LINQ result set containing cleaned up data, with POCOs (plain old C# objects) and no XML. (In essence, I was going to feed this directly into one of our grids through its DataSource property.)

First I loaded the XML file into an XDocument:

XDocument iTunes = XDocument.Load(@"O:\My Music\iTunes\iTunes Music Library.xml");

Then I applied Joshua's LINQ expression to convert the iTunes XML into something more palatable:

var rawtracks = from track in iTunes.Descendants("plist").Descendants("dict").Descendants("dict").Descendants("dict")
                select new XElement("track",
                    from key in track.Descendants("key")
                    select new XElement(
                       ((string)key).Replace(" ", ""),
                       (string)(XElement)key.NextNode)
                    );

Remember, until I enumerate the data from the result set, the only thing that happens with this code is that an expression tree is built. No data has been harmed yet! The result set from this is a list of XElements each of whose names is the contents of the <key> element, and whose content is the node that follows the <key> node.

Next, I want to select the actual tracks from this result set (by "actual" I mean that there is an MP3 file behind the track: for some reason the original XML file has a lot of empty entries) and in doing so I want to create an enumerable list of POCOs. First a couple of helper methods:

    private static string XElementToString(XElement e, string defaultValue) {
      if (e == null) 
        return defaultValue;
      return e.Value;
    }

    private static TimeSpan XElementToTimeSpan(XElement e, TimeSpan defaultValue) {
      if (e == null)
        return defaultValue;
      return new TimeSpan(Int64.Parse(e.Value) * 10000);
    }

And then the LINQ statement:

var tracks = from track in rawtracks
             where track.Element("Location") != null
             select new {
               Artist = XElementToString(track.Element("Artist"), string.Empty),
               Album = XElementToString(track.Element("Album"), string.Empty),
               Name = XElementToString(track.Element("Name"), string.Empty),
               Time = XElementToTimeSpan(track.Element("TotalTime"), defaultTime)
             };

OK, a quick explanation is in order. For each track in the result set I'm creating a new anonymous object with four properties: Artist, Album, Name, and Time. The tracks result set is what I need to attach to the DataSource property of my grid. (Again, executing this last statement does not do anything with the data: that only happens when the grid enumerates the result set.)

Now, this works very well for my original requirement, but there is a big problem for any other code. If I wrap this up in a method to return the tracks object, what type does the method return? It's an IEnumerable<T>, but what is T? The only thing it can return is object, which is not exactly informative at code time.

Enter the Name Anonymous Type refactoring from Refactor! Pro. Place the caret on the new keyword (it's easier to hit than the opening brace, which is another activation site), press the refactor key, and select Name Anonymous Type. After renaming the default name, you will get this for the LINQ statement:

var tracks = from track in rawtracks
             where track.Element("Location") != null
             select new Track(
               XElementToString(track.Element("Artist"), string.Empty), 
               XElementToString(track.Element("Album"), string.Empty), 
               XElementToString(track.Element("Name"), string.Empty), 
               XElementToTimeSpan(track.Element("TotalTime"), defaultTime));

It's created a new type called Track and news up another object of this type for every track found in the result set. And what does Track look like?

    [DebuggerDisplay("\\{ Artist = {Artist}, Album = {Album}, Name = {Name}, Time = {Time} \\}")]
    private sealed class Track : IEquatable<Track> {
      private readonly string artist;
      private readonly string album;
      private readonly string name;
      private readonly TimeSpan time;

      public Track(string artist, string album, string name, TimeSpan time) {
        this.artist = artist;
        this.album = album;
        this.name = name;
        this.time = time;
      }

      public override bool Equals(object obj) {
        if (obj is Track)
          return Equals((Track)obj);
        return false;
      }

      public bool Equals(Track obj) {
        if (obj == null)
          return false;
        if (!EqualityComparer<string>.Default.Equals(artist, obj.artist))
          return false;
        if (!EqualityComparer<string>.Default.Equals(album, obj.album))
          return false;
        if (!EqualityComparer<string>.Default.Equals(name, obj.name))
          return false;
        if (!EqualityComparer<TimeSpan>.Default.Equals(time, obj.time))
          return false;
        return true;
      }

      public override int GetHashCode() {
        int hash = 0;
        hash ^= EqualityComparer<string>.Default.GetHashCode(artist);
        hash ^= EqualityComparer<string>.Default.GetHashCode(album);
        hash ^= EqualityComparer<string>.Default.GetHashCode(name);
        hash ^= EqualityComparer<TimeSpan>.Default.GetHashCode(time);
        return hash;
      }

      public override string ToString() {
        return String.Format("{{ Artist = {0}, Album = {1}, Name = {2}, Time = {3} }}", artist, album, name, time);
      }

      public string Artist {
        get { return artist; }
      }
      public string Album {
        get { return album; }
      }
      public string Name {
        get { return name; }
      }
      public TimeSpan Time {
        get { return time; }
      }
    }

This class is not just a bag o' properties either. Just look at what the refactoring decorates it with:

  • A DebuggerDisplay attribute for use when displaying objects of this type with the debugger. Nice.
  • The IEquatable<T> interface. Objects of this class can be compared equal with the semantics and meaning you expect.
  • Readonly fields. The class is readonly from the start.
  • The Equals() methods to fulfill the IEquatable<T> contract
  • A meaningful GetHashCode() implementation that uses all the class' properties.
  • A good ToString() method that shows all the property values.

(I'll note that this declaration is pretty much identical to what the C# compiler builds for you when it compiles an anonymous type.)

Of course, now the code for the class has been created, I can alter it in any way I see fit. For example, I can change properties to be writable, I can reduce the influence of the GetHashCode() method by removing some of the field values, and so on.

But most of all I can change the return type of my putative method to IEnumerable<Track> and move on. Just brilliant.

2 comment(s)
Matthew MacSuga

Thanks Julian.  I completely forgot about that refractoring, which can really come in handy sometimes.  Nice article!

- Matthew (Twitter:  twitter.com/csharpbydesign)

3 December, 2009
Imar_Spaanjaars

Great refactoring indeed. I especially like the inclusion of the DebuggerDisplay attribute and the ToString overload. But wouldn't it be cleaner to "dry" them both into one?

[DebuggerDisplay("{ToString()}")]

private sealed class Track : IEquatable<Track> { ...}

Imar

7 December, 2009

Please login or register to post comments.