1 chap05
Jason Zhu edited this page 2021-08-18 13:30:11 +10:00

Chapter 5. Essential Language Features

Summary

  • Overview of key C# language features, e.g. Properties, Extention, etc.
  • Combine feature into LINQ, which will be used to query data
  • Razor View Engine

5.1 Essential C# Features

5.1.1 Using Automatically Implemented Properties

Regular Property

C# Property (a language feature, class member) decouple data (i.e. content) from how it's set and retreived

e.g. Defining a Property

public class Product
{
    private string name; // Data

    public string Name // Property
    {
        get { return name; } // Getter
        set { name = value; } // Setter
    }
}
  • where value is a special variable represents the assigned value when setting value using Product1.Name = valueName

e.g. Consuming a Property

using System;

namespace automatically_implemented_properties
{
    class Program
    {
        static void Main(string[] args)
        {
            // create a new Product object
            Product myProduct = new Product();
            
            // set the property value using Setter
            myProduct.Name = "Kayak";
            
            // get the property
            string produtName = myProduct.Name;
            Console.WriteLine("Product name: {0}", produtName);
        }
    }

    public class Product
    {
        // ... Implemented above
    }
}

Summary:

  • Public Property class separate setting/getting from data (proviate variable)
  • Property class has getter & setter to be executed.

Problem of Regular Property format:

  • If there are many data variable in a class, then there will be too many (too verbose) Property members.

Automatically Implemented Property (i.e. Automatic Property)

For simple property as shown below

public class Product {
    private int productID;

    public int ProductID {
        get { return productID; }
        set { productID = value; }
    }
}

can be transferred to Automatic Property as shown below

public class Product
{
    public int ProductID { get; set; }
    ...
}

Key points for using automatic properties:

  1. Don't define the bodies of getter and setter
  2. Don't define the filed that property is backed by (i.e. variable productID for property ProductID)
  3. You can revert from an automatic to a regular property any time in future development

e.g. Reverting automatic to regular property (Name)

public class Product
{
    // Automatic Property
    public int ProductID { get; set; }

    // Regular property
    private string name;
    public string Name
    {
        get { return ProductID + name; }
        set { name = value; }
    }
}

5.1.2 Using Object and Collection Initializers

Object and Collection Initializer can be used to simplify the process of (constructuction of a new object + assign values to properties)

e.g. object creation and value assignment w/o initializer

static void Main(string[] args)
{
    // create a new Product object
    Product myProduct = new Product();
    
    // set the property value
    myProduct.ProductID = 100;
    myProduct.Name = "Kayak";
    myProduct.Description = "A boat for one person";
    myProduct.Price = 275M;
    myProduct.Category = "Watersports";
    
    // process the property
    ProcessProduct(myProduct);
}

private static void ProcessProduct(Product prodParam)
{
    // ... statements to process product in some way
}

e.g. create object and assign value with initializer

static void Main(string[] args)
{
    //  create a new Product object using initializer and method on it directly
    ProcessProduct(new Product // {} behind constructor is initializer
    {
        ProductID = 100,
        Name = "Kayak",
        Description = "A boat for one person",
        Price = 275M,
        Category = "Watersports"
    });
}

private static void ProcessProduct(Product prodParam)
{
    // ... statements to process product in some way
}

where:

{
    ProductID = 100, Name = "Kayak", ...
}

This is the initializer

  • initializer: Braces {} after object constructor, with its values. Values are directly supplied as part of construction process
  • Result of new Product {...} is an instance of Product class. It can then be directly supplied to ProcessProduct method.

e.g. Same feature is used to initialize contents of collection and arrays during construction

static void Main(string[] args) {
    string[] stringArray = { "apple","oragne", "plum" };
    List<int> intList = new List<int> {10, 20, 30, 40};
    Dictionary<string, int> myDict = new Dictionary<string, int> {
        {"apple", 10},
        {"orange", 20},
        {"plum", 30}
    };
}

5.1.3 Extension Methods

Extension methods provide way to add methods to a class that cannot be modified (e.g. 3rd party)

e.g. given following class that can not be modified

public class ShoppingCart {
    public List<Product> Products { get; set; }
}

We can define extension for adding functionality without changing ShoppingCart class

public static class MyExtensionMethod
{
    public static decimal TotalPrices(this ShoppingCart cartParam)
    {
        decimal total = 0;
        foreach (Product prod in cartParam.Products)
        {
            total += prod.Price;
        }
        return total;
    }
}

Syntax:

  • this keyword in front of 1st parameter marks TotalPrices method as an extension method for ShoppingCart class
  • ShoppingCart (first param) tells .NET which class the extension method can be applied to (i.e. ShoppingCart class will be extended)
  • Extention method cannot break access rule of extended classes. public, private, etc. still holds

e.g. Using extension

static void Main(string[] args)
{
    // create and populate Shopping Cart
    ShoppingCart cart = new ShoppingCart
    {
        Products = new List<Product>
        {
            new Product {Name = "Kayak", Price = 275M},
            new Product {Name = "Lifejacket", Price = 48.95M},
            new Product {Name = "Soccer ball", Price = 19.50M},
            new Product {Name = "Corner flag", Price = 34.95M}
        }
    };
    // get total value of the products in cart
    decimal cartTotal = cart.TotalPrices();
    
    Console.WriteLine("Total: {0:c}", cartTotal);

where:

  • cart is object of extended class
  • Use extension by extendedObject.extentionMethod() (i.e. call it as if the extension is original method of extended class)

5.1.4 Applying Extension Methods to an Interface

Extension methods can also be applied to interface, which allows developer to call the extension method on all classes that implement the interface.

Step 1. Modifying data class to implement interface IEnumerable<>

e.g. 5.12 Modifying ShoppingCart Class, so become implementation of interface IEnumerable<Product>. Previously it's class containing List<Product> as member only

using System.Collections;
using System.Collections.Generic;

namespace automatically_implemented_properties
{
    public class ShoppingCart : IEnumerable<Product>
    {
        public List<Product> Products { get; set; }

        public IEnumerator<Product> GetEnumerator()
        {
            return Products.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

Step 2. Modify extention method to deal with interface

e.g. 5.13 An Extension Method That Works on an Interface

    public static class MyExtensionMethod
    {
        public static decimal TotalPrices(this IEnumerable<Product> productEnum)
        {
            decimal total = 0;
            foreach (Product prod in productEnum)
            {
                total += prod.Price;
            }
            return total;
        }
    }

where:

  • 1st parameter is changed to interface IEnumerable<Product>, so this extension can work on every class that implement the interface

Step 3. Modify main program to use MyExtensionMethod to interact with different classes implementing the same interface

        static void Main(string[] args)
        {
            // create and populate Shopping Cart, implementing IEnumerable<Product> interface
            IEnumerable<Product> products = new ShoppingCart
            {
                Products = new List<Product>
                {
                    new Product {Name = "Kayak", Price = 275M},
                    new Product {Name = "Lifejacket", Price = 48.95M},
                    new Product {Name = "Soccer ball", Price = 19.50M},
                    new Product {Name = "Corner flag", Price = 34.95M}
                }
            };
            
            // create and populate an array of Product objects, implementing IEnumerable<Product> interface
            Product[] productArray =
            {
                new Product {Name = "Kayak", Price = 275M},
                new Product {Name = "Lifejacket", Price = 48.95M},
                new Product {Name = "Soccer ball", Price = 19.50M},
                new Product {Name = "Corner flag", Price = 34.95M}
            };
            
            // get total value of the products in cart, using extension method
            decimal cartTotal = products.TotalPrices();
            decimal arrayTotal = productArray.TotalPrices();
            
            Console.WriteLine("Total: {0:c}", cartTotal);
            Console.WriteLine("Array Total: {0:c}", arrayTotal);
        }

5.1.5 Creating Filtering Extension Methods

  • An extension method that operates on an IEnumerable<T> and that also return an IEnumerable<T> can use yield keyword to apply selection criteria to items (in code) to produce a reduced set of results. It's core technique of LINQ
  • Extension methods can be chained together, like LINQ

Step 1. Create filtering extension method

e.g. 5.15 A Filtering Extension Methods

    public static class MyExtensionMethod
    {
        public static decimal TotalPrices(this IEnumerable<Product> productEnum)
        {
            // ...
        }

        public static IEnumerable<Product> FilterByCategory(this IEnumerable<Product> productEnum,
                                                            string categoryParam)
        {
            foreach (Product prod in productEnum)
            {
                if (prod.Category == categoryParam)
                {
                    yield return prod;
                }
            }
        }
    }

where:

  • Entension method FilterByCategory takes categoryParam as second parameter for filtering
  • Those Product objects whose Category property matches the paramter are returned within IEnumerable<Product>

Step 2. Modify main program to use filtering extension method (unchained and chained)

        static void Main(string[] args)
        {
            // create and populate Shopping Cart, implementing IEnumerable<Product> interface
            IEnumerable<Product> products = new ShoppingCart
            {
                Products = new List<Product>
                {
                    new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
                    new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
                    new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
                    new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
                }
            };

            foreach (Product prod in products.FilterByCategory("Soccer"))
            {
                Console.WriteLine("Name: {0}, Price {1:c}", prod.Name, prod.Price);
            }

            // Chained extension methods
            decimal total = products.FilterByCategory("Soccer").TotalPrices();
            Console.WriteLine("Filtered total: {0:c}", total);
        }

5.1.6 Using Lambda Expressions

Purpose of this section: use Lambda Expression or delegate to make filter method more general (i.e. not hardwritten as equal function and can be swapped in runtime)

Using Delegate in extension method

Step 1. Create a filter function that accept delegate Func<Product, bool> selectorParam. Details about how Delegate is defined can refer to Func<T,TResult> Delegate

        public static IEnumerable<Product> Filter(this IEnumerable<Product> productEnum,
            Func<Product, bool> selectorParam)
        {
            foreach (Product prod in productEnum)
            {
                if (selectorParam(prod))
                {
                    yield return prod;
                }
            }
        }

Step 2. Modify main program to use delegate as param for filtering

            
            Func<Product, bool> categoryFilter = delegate(Product prod)
            {
                return prod.Category == "Soccer";
            };

            IEnumerable<Product> filteredProducts = products.Filter(categoryFilter);
            
            foreach (Product prod in filteredProducts) 
            {
                Console.WriteLine("Name: {0}, Price {1:c}", prod.Name, prod.Price);
            }         

Using Lambda Expression in extension method

By MSDN, a lambda expression that has 1 parameter and returns a value can be converted to a Func<T,TResult> delegate. So, switch from delegate to lambda is simple

Step 3: Modify delegate to lambda

            Func<Product, bool> categoryFilter = prod => prod.Category == "Soccer";
            IEnumerable<Product> filteredProducts = products.Filter(categoryFilter);

Step 4: Further simplify lambda to use a lambda expression without Func. (And chain lambda)

           IEnumerable<Product> filteredProducts = products.Filter(prod => 
                prod.Category == "Soccer" || 
                prod.Price > 20);

5.1.7 Using Automatic Type Inference

Type inference/Implicit typing: Compiler will infer the type from the code (i.e. guess datatype based on expression)

var myVariable = new Product {
    Name = "Kayak",
    Category = "Watersports",
    Price = 275M
};

string name = myVariable.Name; // legal
int count = myVariable.Count; // error

As shown above, compilier will generate error

5.1.8 Using Anonymous Types

Combining object initializer and type inference, simple data-storage object can be created w/o defining class or structure clearly

var myAnonType = new {
    Name = "MVC", // string type inference
    Category = "Pattern"
};

where myAnonType is an anonymously typed object (strongly-typed), where its definition will be created automatically by compiler

Note:

  • Compiler generates class based on name (Name, Category) and type (string) of parameter of initializer
  • Hence, 2 anonymously typed objects (having same property name & type) will be assigned to same automatically generated class.

Array of anonymously typed object can also be created using object initializer new[] {...}

  • The array can be enumerated through, even if it's anonymously typed
var oddsAndEnds = new[] {
    new { Name = "MVC", Category = "Pattern" },
    new { Name = "Hat", Category = "Clothing" },
    new { Name = "Apple", Category = "Fruit" }
};

foreach (var item in oddsAndEnds) { // We can enumerate through anonymously type object array
    Console.WriteLine("Name": {0}", item.Name);
}

5.1.9 Performing Language Integrated Queries

All of previously described C# features are used together in LINQ.

  • Automatically Implemented Properties
  • Object and Collection Initializers
  • Extension Methods
  • Extension Methods to Interface
  • Filtering Extension Methods
  • Lambda Expressions
  • Automatica Type Inference
  • Anonymous Types

LINQ = SQL-like syntax for querying data in classes

  • LINQ has query syntax and dot-notation syntax
  • e.g. having a collection of Product objects, want to find 3 products with highest prices, and print out their names and prices

e.g. 5.26 Querying without LINQ

using System;
using System.Collections.Generic;

class Program {
static void Main(string[] args)
{
    Product[] products =
    {
        new Product {Name = "Kayak", Category = "Watersports", Price = 275M},
        new Product {Name = "Lifejacket", Category = "Watersports", Price = 48.95M},
        new Product {Name = "Soccer ball", Category = "Soccer", Price = 19.50M},
        new Product {Name = "Corner flag", Category = "Soccer", Price = 34.95M}
    };

    // -------- Sorting without LINQ ------------
    // define the array to hold results
    Product[] results = new Product[3];
    // sort the contents of the array
    Array.Sort(products, (item1, item2) =>
    {
        return Comparer<decimal>.Default.Compare(item1.Price, item2.Price);
    });
    // get the first three items in the array as the results
    Array.Copy(products, results, 3);
    // -------------------------------------------

    // print out the name
    foreach (Product p in results)
    {
        Console.WriteLine("Item: {0}, Cost: {1}", p.Name, p.Price);
    }
}

e.g. 5.27 Using LINQ query syntax

            var results = from product in products
                orderby product.Price descending
                select new
                {
                    product.Name,
                    product.Price
                };

e.g. 5.27 Using LINQ dot-notation syntax

            var results = products
                .OrderByDescending(e => e.Price)
                .Take(3)
                .Select(e => new {e.Name, e.Price});
  • LINQ dot-notation syntax is not as good-look as query syntax. But not all LINQ feature have corresponding C# keywords.
  • For advanced LINQ usage, there is need to swtich to using extension methods.
  • How LINQ Extention works: each LINQ extension methods is applied to an IEnumerable<T> and returns an IEnumerable<T>. Hence, LINQ extension methods can be chained together to form complex queries.
  • Details about LINQ can refer to MSDN or Oreilly book like C# 9.0 in Nutshell

5.1.9.1 Understanding Deferred Linq Queries

5.1.9.2 Repeatedly Using a Deferred Query

5.2 Understanding Razor Syntax

5.2.1 Creating the Project

5.2.1.2 Defining the Controller

5.2.1.3 Creating the View

5.2.1.4 Setting the Default Route

5.2.2 Examing a Basic Razor View

5.2.3 Working with the Model Object

5.2.4 Including multiple Functions in a Code Block

5.2.5 Passing data using the View Bag Feature

5.2.6 Working with Layouts

5.2.7 Working without Layouts