Goals:

Steps:

  1. Create a new Console Application project in Visual Studio.
  2. Add to the project a new class named FileInfoViewer which will store information about files located in the specified directory and display some information on the console.
  3. To make the work of adding and testing new methods of the FileInfoViewer class easier, write code which will look for 'special' methods and build a menu for the user to allow to execute these methods.
  4. Add to the FileInfoViewer class a private method that will be able to write to the console a list of FileInfo objects. To allow to use the method for any type of collection (e.g. an array or a generic list), take the collection to list as an IEnumerable<FileInfo> object:
    private void ListContent(IEnumerable<FileInfo> toList)
    {
        
    foreach (FileInfo fi in toList)
        {
            
    Console.WriteLine(fi.Format());
        }
    }
  5. Write a public method of the FileInfoViewer class listing files bigger then 1 MB. Remember to apply the RunnableMethodAttribute to the method, to display it automatically in the menu.
    [RunnableMethod]
    public void BiggerThan1MB()
    {
        
    long megabyte = 1024 * 1024;
        
    var chosen =
            
    from fi in filesInfo
            
    where fi.Length > megabyte
            
    select fi;

        ListContent(chosen);
    }
    It is a quite simple query with a single condition.
  6. Now, list all files containing in the name at least one letter from their extensions:
    [RunnableMethod]
    public void ContainingLetterFromExtension()
    {
        
    var chosen =
            
    from fi in filesInfo
            
    where fi.GetNameWithoutExtension().IndexOfAny(fi.Extension.ToCharArray()) >= 0
            
    select fi;

        ListContent(chosen);
    }
    Where the GetNameWithoutExtension method is an extension method (note the this keyword before the first parameter) of the FileInfoExtensions class:
    public static string GetNameWithoutExtension(this FileInfo fi)
    {
        
    return fi.Name.Substring(0, fi.Name.Length - fi.Extension.Length);
    }
  7. List 10 newest files, sorted by the last access time.
    There is no special operator for the Take method, so the normal syntax of calling a method must be used.
    [RunnableMethod]
    public void Top10Newest()
    {
        
    var chosen =
            (
    from fi in filesInfo
            
    orderby fi.LastAccessTime descending
             select fi)
            .Take(10);

        ListContent(chosen);
    }
    Default sorting is ascending, to change the direction, the 'descending' operator must be used.
  8. To show the smallest file with the last access time from the current year, the following code can be used:
    [RunnableMethod]
    public void SmallestFromCurrentYear()
    {
        
    var chosen =
            (
    from fi in filesInfo
            
    where fi.LastAccessTime.Year == DateTime.Now.Year + 10
            
    orderby fi.Length
            
    select fi)
             .FirstOrDefault();

        
    Console.WriteLine(chosen == null ? "no such file" : chosen.Name);
    }
    Note that in case of no files with the last access time from the current year, using First instead FirstOrDefault would throw an exception.
  9. The next task is to answer a question of how many files were last accessed in the current year and in previous years.
    [RunnableMethod]
    public void NumberFilesPerYear()
    {
        
    var chosen =
            
    from fi in filesInfo
            
    group fi by fi.LastAccessTime.Year into byYears
            
    orderby byYears.Key
            
    select new { Year = byYears.Key, Count = byYears.Count() };

        
    foreach (var ch in chosen)
        {
            
    Console.WriteLine("{0}: {1} files", ch.Year, ch.Count);
        }
    }
    The special 'group' operator can be used for grouping selected objects. The object named after the 'into' operator is a collection storing data of one group of objects. It has the special 'Key' property with value of the key of grouping.
  10. The next task is similar to the previous one - group files (this time using their extensions) and show some statistics of groups: number of files, minimum size, and maximum size.
    [RunnableMethod]
    public void MaxMinSizePerExtension()
    {
        
    var chosen =
            
    from fi in filesInfo
            
    group fi by fi.Extension.ToUpper() into g
            
    orderby g.Key
            
    select new {
                Extension = g.Key,
                Count = g.Count(),
                MaxSize = g.Max(a => a.Length),
                MinSize = g.Min(a => a.Length)
            };

        
    foreach (var ch in chosen)
        {
            
    Console.WriteLine("{0,-10}: {1,2} files, min size: {2,8}, max size: {3,8}",
                ch.Extension, ch.Count, ch.MinSize, ch.MaxSize);
        }
    }
    The same expression can be also written using nested select operators:
    [RunnableMethod]
    public void MaxMinSizePerExtension_Alt()
    {
        
    var chosen =
            
    from fi in filesInfo
            
    group fi by fi.Extension.ToUpper() into g
            
    orderby g.Key
            
    select new
            {
                Extension = g.Key,
                Count = g.Count(),
                MaxSize = (
    from a in g select a.Length).Max(),
                MinSize = (
    from a in g select a.Length).Min(),
            };

        
    foreach (var ch in chosen)
        {
            
    Console.WriteLine("{0,-10}: {1,2} files, min size: {2,8}, max size: {3,8}",
                ch.Extension, ch.Count, ch.MinSize, ch.MaxSize);
        }
    }
  11. Of course, not only aggregation functions are available for groups, items contained by the grouped collections can also be available. Write code grouping files by their extensions and listing grouped files:
    [RunnableMethod]
    public void FilesGroupedByExtension()
    {
        
    var chosen =
            
    from fi in filesInfo
            
    group fi by fi.Extension.ToUpper() into g
            
    orderby g.Key
            
    select new { Extension = g.Key, FilesInfo = g };

        
    foreach (var ch in chosen)
        {
            
    Console.WriteLine();
            
    Console.WriteLine("{0}: ", ch.Extension);
            ListContent(ch.FilesInfo);
        }
    }
  12. If we want to see only a list of extensions, there is no need for grouping:
    [RunnableMethod]
    public void ListExtensions()
    {
        
    var chosen =
            (
    from fi in filesInfo
            
    select fi.Extension.ToUpper())
            .Distinct();

        
    foreach (var ch in chosen)
        {
            
    Console.WriteLine(ch);
        }
    }
  13. A rather simple task to answer a quesion of total size of all files can be solved using new LINQ operators:
    [RunnableMethod]
    public void TotalSize()
    {
        
    long totalSize =
            (
    from fi in filesInfo
            
    select fi.Length)
            .Sum();

        
    Console.WriteLine("Total size: {0:0,0}", totalSize);
    }
    or simpler:
    [RunnableMethod]
    public void TotalSize_Alt()
    {
        
    long totalSize = filesInfo.Sum(fi => fi.Length);
        
    Console.WriteLine("Total size: {0:0,0}", totalSize);
    }
  14. More complicated example: list only files containing in their names a name of any other file.
    [RunnableMethod]
    public void ContainingNames()
    {
        
    var chosen =
            
    from lookedFor in filesInfo
            
    from searched in filesInfo
                
    where searched.Name.Contains(lookedFor.Name.Substring(0, lookedFor.Name.Length - lookedFor.Extension.Length)) &&
                    searched.Name != lookedFor.Name
            
    select new { SubName = lookedFor.Name, Name = searched.Name };

        
    foreach (var ch in chosen)
        {
            
    Console.WriteLine("{0}: {1}", ch.SubName, ch.Name);
        }
    }
    In this example the 'where' operator uses an expression using 2 iterated variables. This expression could be simpler in case of using a method returning a name without an extension for a FileInfo object (the GetNameWithoutExtension method from p.6). Using this method, the expression is more readable:
    [RunnableMethod]
    public void ContainingNames_Alt()
    {
        
    var chosen =
            
    from lookedFor in filesInfo
            
    from searched in filesInfo
            
    where searched.Name.Contains(lookedFor.GetNameWithoutExtension()) &&
               searched.Name != lookedFor.Name
            
    select new { SubName = lookedFor.Name, Name = searched.Name };

        
    foreach (var ch in chosen)
        {
            
    Console.WriteLine("{0}: {1}", ch.SubName, ch.Name);
        }
    }

[Source code]