C# 3 Features

Summary

Implicit Variables

An implicitly typed local variable declaration is one where the type of the local variable is inferred from the expression used to initialize the variable. If a local variable declaration specifies var as the type and no type is named while var is in the scope, the declaration is an implicitly-typedlocal variable.

Note the following restrictions:

  1. The declarator must include an initializer

  2. The initializer must be an expression. The initializer cannot be an object or a collection initliazer.

  3. The initializer cannot be null.

The following example tests various var variables. If you place a breakpoint at the beginning of this function, and open the Locals debug window, you will note that all local var variables are correctly typed (i.e., i is int, s is string, etc.):

public void TestImplicitVariables()

{

    /* CORRECT USAGE OF IMPLICITLY TYPED VARIABLES */           

    // Non-array variables

    var i = 5;                      // Same as: int i = 5

    var s = "Hello";                // Same as: string s = "Hello"

    var d = 1.0;                    // Same as: double d = 1.0

 

    // Array variables

    // Same as: int[] numbers = new int[] {1,2,3}

    var numbers = new int[] { 1, 2, 3 };

 

    // Same as: Dictionary<int, string> orders = new Dictionary<int, string>();

    var orders = new Dictionary<int, string>();

 

    /* INCORRECT USAGE OF IMPLICITLY TYPED VARIABLES */

    //var X;                  // error CS0818: Implicitly-typed local variables must be initialized

    //var y = { 1, 2, 3 };    // error CS0820: Cannot initialize an implicitly-typed local variable with an array initializer

    //var z = null;           // error CS0815: Cannot assign <null> to an implicitly-typed local variable

 

    /* USING 'var' IN STATEMENTS */

 

    // The type of v below is inferred to be int, the element type of 'numbers'

    foreach (var v in numbers)

        Trace.WriteLine("Number: " + v);

}

In an implicitly typed array creation expression, the type of the array instance is inferred from the elements specified in the array initializer. Specifically, the set formed by the types of the expressions in the array initializer must contain exactly one type to which each type in the set is implicitly convertible.

public void TestImplicitlyTypedArrays()

{

    // The following statements are equivalent

    int[] nArray1 = new int[] { 1, 2, 3, 4 };

    var   nArray2 = new[] {1,2,3,4};

 

    string[] strArray1 = new string[] { "A", "B", "C" };

    var strArray2 = new[] { "A", "B", "C" };

 

    // The following is incorrect since the elements of the array initializer have two

    // different types, int and string

    //var array1 = new[] { 1, "2", 3 };             // error CS0826: No best type found for implicitly-typed array

    object[] array1 = new object[] { 1, "2", 3 };   // Proper way to fix array1

}

Object Initializers

Typically, an expression to create an object may include an object or collection initializer. This initializer initializes the members of the newly created object, or the elements of the newly created collection. For example, assume class Point has two properties, X and Y. An instance of Point can be created as follows:

Point pt = new Point();
pt.X = 10;
pt.Y = 20;

Or, using object initializers:

Point a = new Point { X = 10, Y = 20 };        // Equivalent to the previous code block

An object initializer specifies values for one or more fields or properties of an object. An object initializer consists of a sequence of member initializers,  enclosed by { and } tokens and separated by commas. Each member initializer must name an accessible field or property of the object being initialized, followed by an equals sign and an expression or an object or collection initializer. It is an error for an object initializer to include more than one member initializer  for the same field or property. For example, consider this object initializer

Point pt3 = new Point {X = 1, Y = 2 };

This effectively creates a new object pt3 of type Point and uses object initiailzer to initialize the object with values from two the properties, X and Y. The following commented examples illustrate further:

public void TestObjectInitializers()

{

    // Old ways of creating and initializing an object

    Point pt2 = new Point();

    pt2.X = 1;

    pt2.Y = 2;

 

    // Object Initializer. Note use of {} rather than ()

    Point pt3 = new Point {X = 1, Y = 2 };

 

    // Old way of creating and initializing an object

    Rectagle rect1 = new Rectagle();

 

    // Object Initializer. Note use of {} rather than ()

    Rectagle rect2 = new Rectagle{ Point1 = new Point{X=10,Y=20}, Point2 = new Point{X=30,Y=40} };

}

 

/// Helper classes to illustrate object and collection initializers

internal class Point

{

    // Data members

    private int _x, _y;

  

    // Properties

    public int X

    {

        get { return _x; }

        set { _x = value; }

    }

    public int Y

    {

        get { return _y; }

        set { _y = value; }

    }

}

 

internal class Rectagle

{

    // Data members

    private Point _pt1;

    private Point _pt2;

 

    // Constructors

    public Rectagle()

    {

        // Initialize points to zeros. Note that we use object-initializer syntax

        _pt1 = new Point { X = 0, Y = 0 };

        _pt2 = new Point { X = 0, Y = 0 };

    }

 

    // Public properties

    public Point Point1

    {

        get { return _pt1;}

        set { _pt1 = value; }

    }

    public Point Point2

    {

        get { return _pt2;}

        set { _pt2 = value; }

    }

}

A collection initializer specifies the elements of a collection, enclosed by { and } tokens, and separated by commas. For example:

List<int> lst1 = new List<int> { 1, 2, 3, 4 };

Note that the collection object to which a collection initializer is applied must be of a type that implements System.Collections.Generic.ICollection<T> for exactly one T. Furthermore, an implicit conversion must exist from the type of each element initializer to T.  Note that a collection initializer invokes the ICollection<T>.Add(T) method for  each specified element in order:

public void TestCollectonInitializers()

{

    // Old way to initialize a list of integers

    List<int> lst1 = new List<int>();

    lst1.Add(1);

    lst1.Add(2);

    lst1.Add(3);

    lst1.Add(4);

 

    // New way

    List<int> lst2 = new List<int> { 1, 2, 3, 4 };

 

    // The following combines object and collection initializers. Note how a List<Point>

    // can be initialzied with a collection initializer where each element is initialized

    //using an object initializer

    List<Point> lstPoints = new List<Point>{ new Point{X=1, Y=1}, new Point{X=2, Y=2}, new Point{X=3, Y=3}};

}

Anonymous Types

Note: Object initializers are the basis of the anonymous type feature and must be understood before working with anonymous types.

C# 3.0 permits the new operator to be used with an anonymous object initializer to create an object of an anonymous type. An anonymous type is a nameless class type that inherits directly from object. It is a type that is auto-generated by the compiler. At compile-time, the compiler creates a new anonymous type with properties inferred from the object initializer. In other words, you do not declare the type in a class, but rather you delegate this responsibility to the compiler by telling it what properties this new type should have. The compiler then generates this type for you without having to write the class. For example, this anonymous type statement:

var an1 = new {ID = 1, Name="Yazan" }

will automatically create a new type which has two properties: int ID and string Name. In other words, the compiler will generate a class similar to this for you without having to write the code:

class _Anonymous1
{
    // Data members
   
private int _id;
    private string _name;

    public int ID
    {
        get {return _id;}
        set {_id = value; }
    }

    public string Name
    {
        get {return _name;}
        set {_name = value;
    }
}

While it may seem that anonymous types save you from defining a class, anonymous types are the cornerstone to LINQ. Without anonymous types, LINQ would not be able to create types dynamically (this is what allows LINQ to query for an arbitrary set of data without first having the structure declared). The following example illustrates anonymous types in more detail:

public void TestAnonymousTypes()

{

    // Create an anonymous type that has two get/set properties: an int ID

    // and a string Name. Note that intellisense shows that the two properties

    // ID and Name in addition to all the methods of class object are available

    // ob object an1. Note that the 'var' keyword is a necessity for variables that

    // refer to instances of anonymous types

    var an1 = new { ID = 1, Name = "Yazan" };

    Trace.WriteLine("ID : " + an1.ID + ". Name : " + an1.Name);    // ID : 1. Name : Yazan

 

    // The name of an anonymous type is automatically generated by the compiler and

    // cannot be referenced in program text

    Trace.WriteLine("Type of an1: " + an1.GetType().Name);          // Type of an1: <>f__AnonymousType0`2

 

    // The following is equivalent to creating an1, except that it requires us to

    // declare the MyType type

    MyType mt1 = new MyType(1, "Yazan");

 

    // Within the same program, two anonymous object initializers that specify a sequence of

    // properties of the same names and types in the same order will produce instances of the

    // same anonymous type

    var an2 = new { ID = 1};

    var an3 = new { ID = 3 };

    an3 = an2;

    Trace.WriteLine("Type of an2: " + an2.GetType().Name);  // Type of an2: <>f__AnonymousType1`1

    Trace.WriteLine("Type of an3: " + an3.GetType().Name);  // Type of an3: <>f__AnonymousType1`1

 

    // an4 is not the same as an2 or an3 since the property name is different

    var an4 = new { NAME = "Yazan" };

    //an4 = an3;        // error CS0029: Cannot implicitly convert type 'AnonymousType#1' to 'AnonymousType#2'

 

    // The following shows how to use anonymous types with implicitly-typed arrays to create

    // anonymously-typed data structures

    var an5 = new[] { new { ID = 1, Name = "C" }, new { ID = 2, Name = "B" } };

    Trace.WriteLine("Array length: " + an5.Length);                    // Array length: 2

    Trace.WriteLine("First row: " + an5[0].ID + "/" + an5[0].Name);    // First row: 1/C

    Trace.WriteLine("Second row: " + an5[1].ID + "/" + an5[1].Name);    // Second row: 2/B     

}

 

/// <summary>

/// Helper class used to illustrate anonymous types

/// </summary>

internal class MyType

{

    // Data members

    private int _id;

    private string _name;

 

    // Constructors

    public MyType(int i, string s)

    {

        ID = i;

        Name = s;

    }

 

    // Properties

    public int ID

    {

        get { return _id; }

        set { _id = value; }

    }

 

    public string Name

    {

        get { return _name; }

        set

        {

            _name = value;

        }

    }

}

Extension Methods

With extension methods third parties may augment the public contract of a type with new methods while still allowing individual type authors to provide their own specialized implementation of those methods. Therefore, extension methods are used to extend existing types and constructed types with additional methods.

Syntactically, extension methods are static methods that can be invoked using instance method. extension methods can only be declared in static classes. The first parameter of any extension method must be the this' keyword, which identifies the type on which the extension method will appear. Extension methods are imported using namespace directives. For example, this extension method:

public static int GetCharacterCount(this string s);

means that it will appear on any string type:

string s = "Hello Extension Methods";
int nCount = s.GetCharacterCount();

Note that extension methods are less discoverable and more limited in functionality than instance methods. Therefore, extension methods should be used sparingly and in situations where instance methods are not possible. Extension methods are given the lowest priority in terms of resolution and are only  used if there is no suitable match on the target type and its base types. Therefore, if the normal processing of the invocation finds no applicable instance method, the CLR attempts to process the invocation as an extension method. This effectively means that instance methods take precedence over extension methods. Also extension methods imported in inner namespace declarations take precedence over extension methods imported in outer namespace declarations.

The following example illustrates the extension methods basic functionalities:

public static class MyExtensions        // Note class is static

{

    // This extension method will appear as an instance method on string variables

    public static int GetCharacterCount(this string s)

    {

        Trace.WriteLine("MyExtensions.GetCharacterCount()");

        return s.Length;

    }

 

    // This extension method will appear as an instance method on any array

    // of any type. Assuming the contents of the array are numeric, it adds the

    // contents of the array starting from index nStart upto nCount elements

    //

    public static T Sum<T>(this T[] source, int nStart, int nCount, Calculator<T> calc) where T: struct

    {

        Trace.WriteLine("MyExtensions.Sum<T>()");

        // Check invariants

        if ((nStart < 0) || (nCount < 0) || (source.Length - nStart < nCount))

            throw new ArgumentException("Invalid input data");

 

        T nSum = default(T);

        for (int i = nStart; i < nStart + nCount; i++)

            nSum = calc.Add(nSum, source[i] );

 

        return nSum;

    }

 

    // This extension method is used to test extension method precedence. Note that the

    // first parameter is of type 'object', which means that this method is available

    // as an extension method on every object

    public static int F(this object obj, string sMsg)

    {

        Trace.WriteLine("MyExtensions.F()");

        return sMsg.Length;

    }

}

 

/* Derived classes to test extension method precedence */

// No instance methods. Hence extension methods take precedence

internal class A { /* No extension methods*/ }

 

// B's instance methods take precedence over extension methods

internal class B

{

    public int F(string s)

    {

        Trace.WriteLine("B.F()");

        return s.Length;

    }

}

 

// B's instance methods take precedence over extension methods

internal class C

{

    public int F(object o)

    {

        Trace.WriteLine("C.F()");

        return Convert.ToString(o).Length;

    }

 

}

 

// An abstract class that will be specialized for each generic type that wishes to

// implement calculation functionalities (adding, substracing, division, etc.)

public abstract class Calculator<T>

{

    public abstract T Add(T lhs, T rhs);

}

 

public class IntCalculator : Calculator<int>

{

    public override int Add(int lhs, int rhs)

    {

        return lhs + rhs;

    }

}

 

public class DoubleCalculator : Calculator<double>

{

    public override double Add(double lhs, double rhs)

    {

        return lhs + rhs;

    }

}

The following class shows how to use the extension methods defined in MyExtensions class:

public class ExtensionMethods

{

    public void TestExtensions()

    {

        // Test GetCharacterCount() which is an extension method on 'string'. The following

        // two methods are equivalent in result

        string s = "Hello Extensions";

        int nCount1 = s.GetCharacterCount();                // nCount = 16.

        int nCount2 = MyExtensions.GetCharacterCount(s);    // nCount = 16.

 

        // Test Sum() which is an extension method on any array. The following

        // two methods are equivalent in result

        double[] dNumbers = new double[] { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };

        double dSum1 = dNumbers.Sum(5, 3, new DoubleCalculator());

        double dSum2 = MyExtensions.Sum(dNumbers, 5, 3, new DoubleCalculator());

    }

 

    public void TestExtensionPrecedence()

    {

        A a = new A();      // MyExtensions.F()

        a.F("From A");

 

        B b = new B();      // B.F()

        b.F("From B");

 

        C c = new C();      // C.F()

        c.F("From C");

    }

}

Lambda Expressions

Recall that anonymous methods in C# 2.0 allows code blocks to be written in line where delegate values are expected. In C#' 3.0 Lambda expressions provide a more concise, functional syntax for writing anonymous methods.  A lambda expression is written as a parameter list, followed by the => token, followed by an expression or a statement block. The following are two simple Lambda expressions:

(int x) => x+1;        // Given x, the result is x + 1
(x,y) => x * y;        // Given x and y, the result is x * y

Lambda expressions are a functional superset of anonymous methods, providing the following additional functionality:

The following examples illustrate the basic functionality of Lambda expressions:

// Data members. Various delegate types required to test lambda expressions

private delegate bool delPredicate(string match);

private delegate void delF(int nID, string strName);

private delegate void delSingleParam(double d);

public void TestBasicLambdaExpressions()

{

    // Create and populate a string list with test data

    List<string> sList = new List<string>();

    sList.Add("A");

    sList.Add("A");

    sList.Add("B");

    sList.Add("B");

    sList.Add("C");

    sList.Add("D");

 

    // C# 1.0: Declare a delegate type, define a method that matches the delegate

    // type, instantiate the deleage and invoke it           

    List<string> lstMatches1 = sList.FindAll(PredicateImpl);

 

    // C# 2.0: Anonymous methods allow you to create and invoke the delegate inline.

    // No need to define a delegate function such as PredicateImpl

    List<string> lstMatches2 = sList.FindAll( delegate(string match)

                                              {

                                                return (match.Equals("A"));

                                              }

                                            );

 

    // C# 3.0: A delegate is now an anonymous method with easier syntax

    List<string> lstMatches3 = sList.FindAll( (string match) => match.Equals("A") );

}

 

/// <summary>

/// Tests more basic functionality

/// </summary>

public void TestBasicLambdaExpressions2()

{

    // C# 2.0:  Initialize and invoke a delegate. Note that we need to have a

    // delFImpl function with required implementation

    delF dF1 = delFImpl;

    dF1(1, "Yazan");            // ID: 1/Name: Yazan

 

    // C# 3.0:  Initialize and invoke a delegate. No need to declare a new function

    // with a required implemenation

    delF dF2 = (int id, string name) => Trace.WriteLine("(dF2) ID: " + id + "/Name: " + name);

    dF2(1, "Yazan");            // (dF2) ID: 1/Name: Yazan

 

    // The parameters of a lambda expression can be implicitly or explicitly typed.

    // Previous examples illustrate explicitly-typed parameters. In an implicitly-typed

    // parameter list, the types of the parameters are inferred from the context of the

    // lambda expression:

    delF dF3 = (id, name) => Trace.WriteLine("(dF3) ID: " + id + "/Name: " + name);

    dF3(1, "Yazan");            // (dF3) ID: 1/Name: Yazan

 

    // In a lambda expression with a single implicitly-typed parameter, the parenthesis

    // may be omitted from the parameter list

    delSingleParam dF4 = (double d) => Trace.WriteLine("Type = " + d.GetType().Name + ". Value = " + d);

    delSingleParam dF5 = d => Trace.WriteLine("Type = " + d.GetType().Name + ". Value = " + d);

    dF4(10.0);                  // Type = Double. Value = 10

    dF5(10);                    // Type = Double. Value = 10

    //dF5("Test");                // Error. Argument '1': cannot convert from 'string' to 'double'

}

 

// This is a delegate implementation to illustrate how delegates were used in C# 1.0

private bool PredicateImpl(string match)

{

    return (match.Equals("A"));

}

 

private void delFImpl(int nID, string strName)

{

    Trace.WriteLine("ID: " + nID + "/Name: " + strName);

}

Conversion Rules

A lambda-expression is classified as a value with special conversion rules. The value does not have a type but can be implicitly converted to a compatible delegate type. Specifically, a delegate type D is compatible with a lambda-expression L provided:

  1. D and L have the same number of parameters.

  2. If L has an explicitly typed parameter list, each parameter in D has the same type and modifiers as the corresponding parameter in L.

  3. If L has an implicitly typed parameter list, D has no ref or out parameters.

  4. If D has a void return type and the body of L is an expression, when each parameter of L is given the type of the corresponding parameter in D, the body of L is a valid expression that would be permitted as a statement-expression (8.6).

  5. If D has a void return type and the body of L is a statement block, when each parameter of L is given the type of the corresponding parameter in D, the body of L is a valid statement block in which no return statement specifies an expression.

  6. If D has a non-void return type and the body of L is an expression, when each parameter of L is given the type of the corresponding parameter in D, the body of L is a valid expression that is implicitly convertible to the return type of D.

  7. If D has a non-void return type and the body of L is a statement block, when each parameter of L is given the type of the corresponding parameter in D, the body of L is a valid statement block with a non-reachable end point in which each return statement specifies an expression that is implicitly convertible to the return type of D.

The following example illustrates:

private delegate R GenericFunc<A, R>(A args);

public void TestLambdaExpressionConversions()

{

    // Conditions 1, 3 & 6 apply here: When x is a given type int, 'x+1' is a valid expression

    // that is implicitly convertible to type int.

    Func<int, int> f1 = x => x + 1;

    int nResult = f1(10);

 

    // Conditions 1, 3 & 6 apply here: When x is a given type int, 'x+1' is a valid expression

    // that is implicitly convertible to type double.

    Func<int, double> f2 = x => x + 1;

    double dResult = f2(10);

 

    // Error: When x is double, the result of 'x+1' is also a double, but it is not

    // implicitly convertible to int. The following compile-time error is generated:

    // Cannot convert lambda expression to delegate type 'System.Func<double,int>'

    // because some of the return types in the block are not implicitly convertible

    // to the delegate return type

    //Func<double, int> f3 = x => x + 1;

}

Type Inference

When a generic method is called without specifying type arguments, a type inference process attempts to infer the type arguments for the call. Lambda expressions passed as arguments to generic methods, participate in this type-inference process. The following details are nice to know:


Inferences are made as long as one or more arguments exist for which all of the following are true:

  1. The argument is a lambda expression, in the following called L, from which no inferences have yet been made.
  2. The corresponding parameter's type, in the following called P, is a delegate type with a return type that involves one or more method type parameters.
  3. P and L have the same number of parameters, and each parameter in P has the same modifiers as the corresponding parameter in L, or no modifiers if L has an implicitly typed parameter list.
  4. P's parameter types involve no method type parameters or involve only method type parameters for which a consistent set of inferences have already been made.
  5. If L has an explicitly typed parameter list, when inferred types are substituted for method type parameters in P, each parameter in P has the same type as the corresponding parameter in L.
  6. If L has an implicitly typed parameter list, when inferred types are substituted for method type parameters in P and the resulting parameter types are given to the parameters of L, the body of L is a valid expression or statement block.

For each such argument, inferences are made from that argument by relating the return type of P with the inferred return type of L and the new inferences are added to the accumulated set of inferences. This process is repeated until no further inferences can be made. A return type can be inferred for L, as described below:

  1. If the body of L is an expression, the type of that expression is the inferredcreturn type of L.
  2. If the body of L is a statement block, if the set formed by the types of the expressions in the block's return statements contains exactly one type to which each type in the set is implicitly convertible, and if that type is not the null type, then that type is the inferred return type of L.
  3. Otherwise, a return type cannot be inferred for L.

The following example uses the .NET Standard Query Operators which is an API that enables querying of any .NET array or collection. The Standard Query Operators API  consists of the methods declared in the System.Query.Sequence static class in the  assembly named System.Query.dll.  The query operator that is used is Select which has the following signature:

static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector);

The first argument shows that this is an extension method that is available on any object that implements the generic IEnumerable<T>. For example, this method is  available on List<T> which implements IEnumerable<T>. This then implies that T is  TSource. For example, assume we have a class called Customer. A List<Customer> object implements IEnumerable<Customer>, and TSource is  therefore, Customer.
 

The second argument is a delegate that takes a single parameter of type TSource, 'Customer' in this example, and returns a value whose type is TResult. Given that the lambda expression will be c => c.Name (where Name is property on class Customer) then since the body of the Lambda expression is an expression, the type of that expression is the inferred return type of L, i.e., string (which is the type of Customer.Name property.)

private delegate TResult selector<T, TResult>(T arg);

public void TestLambdaExpressionTypeInference()

{

    List<string> lstNames = new List<string>();

    lstNames.Add("AAA");

    lstNames.Add("BBBBB");

    lstNames.Add("CCC");

    lstNames.Add("DDDDDDD");

 

    // Since lstNames is a List<String>, Func<TSource, TResult> is a delegate

    // with a single parameter of type string (TSource). TResult is inferred from

    // the lambda expression to be int. The following two statements are identical.

    IEnumerable<int> lstSelected1 = lstNames.Select( item => item.Length);

    IEnumerable<int> lstSelected2 = lstNames.Select((string item)=> item.Length);

}

Query Expressions

Query expressions allow you to query collections in a manner similar to relational and hierarchical query languages such as SQL and XPath/XQuery. In fact, query expression syntax is very similar to SQL with select, from and where clauses However, the placement of the from clause needs to be first in order to determine the type and to enable scenarios like Intelli-Sense support in Visual Studio 2008. For example, consider this very simple query expression which acts on a collection of Customer objects:

// Create an populate a collection of Customers
List<Customers> customers = new List<Customers>();
customers.Add( ... );
customers.Add( ... );


// create a query on the customers collection to select all Customer objects whose ID is > 1
var qe1 = from c in customers where c.CustomerId > 0  select c;

// Iterate on qe1 to retrieve data
foreach (Customer c in qe1)
    Trace.WriteLine( "ID:" + c.ID );

As shown above, a query expression begins with a from clause and ends with either a select or group clause. The initial from clause can be followed by zero or more from or where clauses. Each from clause is a generator that introduces an iteration variable ranging over a sequence, and each where clause is a filter that excludes items from the result. The final select or group clause specifies the shape of the result in terms of the iteration variable(s). The select or group clause may be preceded by an orderby clause that specifies an ordering for the result. Finally, an into clause can be used to "splice" queries by treating the results of one query as a generator in a subsequent query. The following example illustrates these concepts:

public void TestBasicExpressions()

{

    // Create and populate a collection of customers

    List<Customer> customers = new List<Customer>();

    customers.Add(new Customer { ID = 5, Name = "E" });

    customers.Add(new Customer { ID = 3, Name = "C" });

    customers.Add( new Customer { ID = 1, Name="A" } );

    customers.Add( new Customer { ID = 2, Name = "B" });           

    customers.Add(new Customer { ID = 4, Name = "D" });

 

    // Create a query on customers to select only those customers whose ID is >= 2

    // q1 will be a type that implements

    // System.Collections.Generic.IEnumerable<CSharpNewFeatures.Customer>, among others.

    var q1 = from customer in customers

            where customer.ID >= 2

            orderby customer.Name

            select customer;

 

    // We can now iterate the results of the query.

    foreach (Customer c in q1)

        Trace.WriteLine("ID: " + c.ID);

}

 

/// <summary>

/// Helper class to illustrate query expressions

/// </summary>

internal class Customer

{

    // Data members

    private int _id;

    private string _name;

 

    // Properties

    public int ID

    {

        get { return _id; }

        set { _id = value; }

    }

 

    public string Name

    {

        get { return _name; }

        set { _name = value; }

    }

}

C# 3.0 translates query expressions into invocations of methods that adhere to the query expression pattern. Specifically, query expressions are translated into invocations of methods named Where, Select, SelectMany, OrderBy, OrderByDescending, ThenBy, ThenByDescending, and GroupBy that are expected to have particular signatures and result types.

Following translation of query expressions, the resulting method invocations are processed as regular method invocations, and this may in turn uncover errors, for example if the methods do not exist, if arguments have wrong types, or if the methods are generic and type inference fails. The following example illustrates:

public void TestQueryExpressionTranslation()

{

    // Create and populate a collection of customers. Note the use of object and collection

    // initiailzers

    List<Product> products = new List<Product>();

    products.Add(new Product { ID = 5, Name = "E", Category = "C1", Stores = { "Store1","Store2","Store3"} });

    products.Add(new Product { ID = 3, Name = "C", Category = "C2", Stores = { "Store1", "Store2", "Store3" } });

    products.Add(new Product { ID = 1, Name = "A", Category = "C2", Stores = { "Store1", "Store2", "Store3" } });

    products.Add(new Product { ID = 2, Name = "B", Category = "C3", Stores = { "Store1", "Store2", "Store3" } });

    products.Add(new Product { ID = 4, Name = "D", Category = "C1", Stores = { "Store1", "Store2", "Store3" } });

    products.Add(new Product { ID = 6, Name = "F", Category = "C1", Stores = { "Store1", "Store2", "Store3" } });

 

    // Example Translation 1 - Generic: q1 same as eLst1

    var q1 = from product in products where product.ID == 1 select product;

    IEnumerable<Product> eLst1 = products.Where(product => product.ID == 1);

    foreach (Product prod in eLst1) Trace.WriteLine(prod.ID + "/" + prod.Name + "/" + prod.Category);

 

    // Example Translation 2 - Where: q2 same as eLst2

    var q2 = from product in products where product.Name == "B" select product;

    IEnumerable<Product> eLst2 = products.Where(product => product.Name == "B").Select(c => c);

    foreach (Product prod in eLst2) Trace.WriteLine(prod.ID + "/" + prod.Name + "/" + prod.Category);

 

    // Example Translation 3 - OrderBy: q3 same as eLst3

    var q3 = from product in products orderby product.ID select product;

    IEnumerable<Product> eLst3 = products.OrderBy(product => product.ID);

    foreach (Product prod in eLst3) Trace.WriteLine(prod.ID + "/" + prod.Name + "/" + prod.Category);

 

    // Example Translation 4 - GroupBy: q4 same as eLst4. Note the return value of GroupBy<>()

    var q4 = from product in products group product by product.Category;

    IEnumerable<IGrouping<string, Product>> eLst4 = products.GroupBy(product => product.Category);

    foreach (Product prod in eLst4) Trace.WriteLine(prod.ID + "/" + prod.Name + "/" + prod.Category);

 

    // Example Translation 5 - multiple generators:

    var q5 = from product in products from store in product.Stores where store == "Store1" select product;

    IEnumerable<string> eLst5 = products.SelectMany(product => product.Stores.Where(store => store == "Store1"));

    foreach (string store in eLst5) Trace.WriteLine(store);

}

 

/// <summary>

/// Helper class to illustrate query expressions

/// </summary>

internal class Product

{

    // Data members

    private int _id;

    private string _name;

    private string _category;

    private List<string> _lstStores;

 

    // Properties

    public int ID

    {

        get { return _id; }

        set { _id = value; }

    }

 

    public string Name

    {

        get { return _name; }

        set { _name = value; }

    }

 

    public string Category

    {

        get { return _category; }

        set { _category = value; }

    }

 

    public List<string> Stores

    {

        get { return _lstStores; }

        set { _lstStores = value; }

    }

}