Multitherading, Collections, LINQ in CSharp language.

Multitherading, Collections, LINQ in CSharp language.

Multithreading enables concurrent execution of code, improving performance and responsiveness. Collections manage groups of objects, like arrays and lists. LINQ provides a query language for data manipulation, enhancing data retrieval and processing.


Multitherading:

Before we go to into multithreading, let's discuss multitasking:

Multitasking:

  • Windows OS is a multitasking system, allowing multiple tasks to run at the same time. But how do all these tasks run at the same time?

  • To execute multiple programs, the OS uses processes. A process is a part of the OS responsible for executing a program.

  • If you look at the task manager, you'll see many processes running. Each process executes one program.

  • There are also many background processes, which are essentially Windows services.

  • Besides these, multiple applications can be running, with each process running one application.

Operating System and Threads:

  • To run an application, a process uses threads.

  • A thread is the smallest unit of execution within a process.

  • Every application has logic to be executed, and threads are responsible for executing this logic.

  • Every application must have at least one thread to execute, which is known as the main thread. This means every application is by default a single-threaded model.

Main Thread:

  • Every application by default contains one thread to execute the program, known as the main thread.

  • By default, every program runs in a single-threaded model.

Multithreading in Detail:

  • Multithreading allows an application to perform multiple operations concurrently by creating multiple threads within the same process.

  • This can significantly improve the performance of an application, especially on multi-core processors where threads can run on different cores simultaneously.

Key Points

  • Processes: High-level entities used by the OS to manage the execution of programs.

  • Threads: Low-level units of execution within processes.

  • Single-threaded: By default, applications run on a single main thread.

  • Multi-threaded: Applications can create additional threads to run multiple tasks simultaneously.

Example 1: Single-threaded Program

using System;
class ThreadProgram
{
    static void Main()
    {
        Console.WriteLine("Hello world");
    }
}
// Output: Hello world

This program contains a thread, and with the help of this thread, the program executes. This thread is known as the main thread.

Example 2: Naming the Main Thread

using System;
using System.Threading;

class ThreadProgram
{
    static void Main()
    {
        Thread t = Thread.CurrentThread;
        t.Name = "MyThread";
        Console.WriteLine("Now current thread is: " + t.Name);
    }
}
// Output: Now current thread is: MyThread

In this example, we demonstrate that there is a thread running the program. Every program by default runs with a single thread.

Explanation

In the above examples:

  • Example 1: The program runs using the main thread by default.

  • Example 2: We explicitly name the main thread to show that the program runs on a single thread, which we've named "MyThread".

Single-threaded Disadvantage:

  • In a single-threaded program, all logic runs in one thread.

  • If your program contains multiple methods, and all methods are called by the main method, the main thread is responsible for executing all methods.

  • This means the main thread executes each method one by one, sequentially.

Example to Illustrate the Disadvantage:

using System;

class ThreadProgram
{
    static void Main()
    {
        Method1();
        Method2();
        Method3();
    }

    static void Method1()
    {
        Console.WriteLine("Executing Method1");
    }

    static void Method2()
    {
        Console.WriteLine("Executing Method2");
    }

    static void Method3()
    {
        Console.WriteLine("Executing Method3");
    }
}
// Output:
// Executing Method1
// Executing Method2
// Executing Method3

In this example:

  • The main thread calls Method1, Method2, and Method3 sequentially.

  • The main thread is responsible for executing all methods, one after the other.

  • This sequential execution can be a disadvantage if you have tasks that could be performed concurrently to save time or improve performance.

Issue with Single-threaded Programs

In a single-threaded program, if any method takes a long time to finish, the whole program has to wait for that method before moving to the next task. This can waste time and resources. For example, if a method is waiting for a response from a busy database, the entire program is stuck waiting for that response.

using System;
class ThreadProgram
{
    static void LongRunningMethod()
    {
        //Thread going to sleep
        // Simulate a long-running task, such as a database call
        Console.WriteLine("Starting LongRunningMethod...");
        System.Threading.Thread.Sleep(5000); // main thread going to sleep 'Sleep is a ststic method'
        Console.WriteLine("LongRunningMethod completed.");
    }

    static void Method2()
    {
        Console.WriteLine("Executing Method2");
    }

    static void Method3()
    {
        Console.WriteLine("Executing Method3");
    }

    static void Main()
    {
        //Main thread execution start
        LongRunningMethod();
        Method2();
        Method3();
        //Main thread execution end
    }
}
// Output:
// Starting LongRunningMethod...
// (Waits for 5 seconds)
// LongRunningMethod completed.
// Executing Method2
// Executing Method3

To overcome this issue, we can use multiple threads to allow other parts of the program to continue running while waiting for the long-running method to complete. This is called Multithreading.

In multithreading, when we use multiple threads in a program, the OS distributes CPU time for each thread to execute. Based on time-sharing, all the threads execute equally. In the above example, if you apply multithreading, all methods execute together. Multithreading maximizes CPU resource utilization.

All the threads do not execute in parallel; they execute simultaneously. Where the OS allocates small time slices to each thread, allowing them to run as if they are executing at the same time.

Time-Sharing in Multithreading

  • Time-Sharing: The operating system splits the CPU time among all active threads. Each thread gets a small time slice, called a quantum, to run.

  • Thread Scheduling: The OS uses a scheduler to decide which thread runs at any moment. This makes sure all threads get a fair amount of CPU time.

  • Simultaneous Execution: Threads may not run in parallel (unless on a multi-core processor), but they switch quickly between each other, giving the impression of parallel execution.

How CPU Time Distribution Works

  • Imagine there are three methods (Method1, Method2, and Method3) running on separate threads.

  • The CPU gives a specific amount of time (e.g., 2 seconds) to each thread.

  • During its time, a thread runs its task. If it doesn't finish in that time, the CPU pauses it and moves to the next thread.

  • This process repeats, with the CPU cycling through the threads, giving each one time until all tasks are done.

  • The operating system, not the program, decides the length of the time slice.

This means all methods are given equal priority to execute.

using System;
using System.Threading;

class ThreadProgram
{
    static void Method1()
    {
        for(int i= 1; i <= 20; i++)
        {
            Console.WriteLine("Test1: " + i);
        }
    }
    static void Method2()
    {
        for(int i= 1; i <= 20; i++)
        {
            Console.WriteLine("Test2: " + i);
        }
    }
    static void Method3()
    {
        for(int i= 1; i <= 20; i++)
        {
            Console.WriteLine("Test3: " + i);
        }
    }
    static void Main()
    {
        // Create threads that will execute the methods
        Thread T1 = new Thread(Method1);//Pass method name
        Thread T2 = new Thread(Method2);
        Thread T3 = new Thread(Method3);

        // We do not need to call the methods explicitly because the threads will automatically call them.
        // However, we need to start the threads to begin their execution.
        T1.Start();
        T2.Start();
        T3.Start();
    }
}

/*
CPU share the time: Som time Test1 start some time test2 or sum time test3 like sharing the time:-
Out:-
Test1: 1
Test3: 1
Test2: 1
Test1: 2
Test1: 3
Test1: 4
Test1: 5
Test1: 6
Test1: 7
Test1: 8
Test2: 2
Test2: 3
Test2: 4
Test2: 5
Test2: 6
Test2: 7
Test2: 8
Test2: 9
Test2: 10
Test2: 11
Test3: 2
Test3: 3
Test3: 4
Test3: 5
Test3: 6
Test3: 7
Test3: 8
Test3: 9
Test3: 10
Test3: 11
Test3: 12
Test3: 13
Test3: 14
Test3: 15
Test3: 16
Test3: 17
Test3: 18
Test3: 19
Test3: 20
Test2: 12
Test2: 13
Test2: 14
Test2: 15
Test2: 16
Test2: 17
Test1: 9
Test1: 10
Test1: 11
Test1: 12
Test1: 13
Test1: 14
Test1: 15
Test1: 16
Test1: 17
Test1: 18
Test1: 19
Test1: 20
Test2: 18
Test2: 19
Test2: 20
*/
  • Every time you got diffrence output.

Explanation

  1. Method Definitions:

    • Method1, Method2, and Method3 are simple methods that print out "Test1", "Test2", and "Test3" respectively.
  2. Creating Threads:

    • Thread T1 = new Thread(Method1);: This creates a new thread that will execute Method1.

    • Thread T2 = new Thread(Method2);: This creates a new thread that will execute Method2.

    • Thread T3 = new Thread(Method3);: This creates a new thread that will execute Method3.

  3. Starting Threads:

    • T1.Start();: This starts the execution of T1, which will call Method1.

    • T2.Start();: This starts the execution of T2, which will call Method2.

    • T3.Start();: This starts the execution of T3, which will call Method3.

  4. Stopping the Thread:

    • If you want to stop or terminate a thread, use the Abort() method. For example, to terminate the T1 thread, use T1.Abort();.

Key Points

  • Thread Creation: Threads are created by passing the method to be executed to the Thread constructor.

  • Automatic Method Call: When you start a thread using the Start method, the thread will automatically call the method specified during its creation.

  • Starting Threads: To begin the execution of the threads, you must call the Start method on each thread instance. This does not require you to call the methods directly; the threads handle that automatically.

One more example to better clarify that when something happens with one method, it does not affect another method.

using System;
using System.Threading;

class ThreadProgram
{
    static void Method1()
    {
    for(int i= 1; i <= 20; i++)
    {
        Console.WriteLine("Test1: " + i);
        if(i == 10)
        {
        Console.WriteLine("Thread 2 is going to sleep.");
        Thread.Sleep(1000);
        Console.WriteLine("Thread 2 is wokep now.");
        }
    }
    }
    static void Method2()
    {
    for(int i= 1; i <= 20; i++)
    {
        Console.WriteLine("Test2: " + i);
    }
    }
    static void Method3()
    {
    for(int i= 1; i <= 20; i++)
    {
        Console.WriteLine("Test3: " + i);
    }
    }
    static void Main()
    {
    Thread T1 = new Thread(Method1);
    Thread T2 = new Thread(Method2);
    Thread T3 = new Thread(Method3);
    T1.Start();
    T2.Start();
    T3.Start();
    }
}
/*Out:-
Test2: 1
Test3: 1
Test3: 2
Test3: 3
Test3: 4
Test3: 5
Test3: 6
Test3: 7
Test3: 8
Test3: 9
Test3: 10
Test3: 11
Test3: 12
Test3: 13
Test3: 14
Test1: 1
Test3: 15
Test3: 16
Test2: 2
Test2: 3
Test2: 4
Test2: 5
Test1: 2
Test2: 6
Test2: 7
Test2: 8
Test3: 17
Test3: 18
Test3: 19
Test3: 20
Test1: 3
Test1: 4
Test1: 5
Test1: 6
Test2: 9
Test2: 10
Test2: 11
Test2: 12
Test2: 13
Test2: 14
Test2: 15
Test2: 16
Test2: 17
Test2: 18
Test2: 19
Test2: 20
Test1: 7
Test1: 8
Test1: 9
Test1: 10
Thread 2 is going to sleep.
Thread 2 is wokep now.
Test1: 11
Test1: 12
Test1: 13
Test1: 14
Test1: 15
Test1: 16
Test1: 17
Test1: 18
Test1: 19
Test1: 20
*/
using System;
using System.Threading;

class ThreadProgram
{
    static void Method1()
    {
    for(int i= 1; i <= 10; i++)
    {
        Console.WriteLine("Test1: " + i);
    }
    Console.WriteLine("Test1 thread execute");
    }
    static void Method2()
    {
    for(int i= 1; i <= 10; i++)
    {
        Console.WriteLine("Test2: " + i);
    }
    Console.WriteLine("Test2 thread execute");
    }
    static void Method3()
    {
    for(int i= 1; i <= 10; i++)
    {
        Console.WriteLine("Test3: " + i);
    }
    Console.WriteLine("Test3 thread execute");
    }
    static void Main()
    {
    Thread T1 = new Thread(Method1);
    Thread T2 = new Thread(Method2);
    Thread T3 = new Thread(Method3);
    T1.Start();
    T2.Start();
    T3.Start();
    Console.WriteLine("Main thread execute");
    }
}
/*Out:-
Main thread execute
Test3: 1
Test1: 1
Test1: 2
Test1: 3
Test1: 4
Test1: 5
Test1: 6
Test1: 7
Test1: 8
Test1: 9
Test1: 10
Test1 thread execute
Test2: 1
Test3: 2
Test3: 3
Test2: 2
Test2: 3
Test2: 4
Test2: 5
Test2: 6
Test2: 7
Test2: 8
Test2: 9
Test2: 10
Test2 thread execute
Test3: 4
Test3: 5
Test3: 6
Test3: 7
Test3: 8
Test3: 9
Test3: 10
Test3 thread execute
*/

Execution Flow:

The Main method is executed by the main thread of the application. The main thread executes first, and then the child threads start executing because the main thread's job is done.

Child Threads:

T1, T2, and T3 are started almost simultaneously. The operating system's scheduler manages the execution of these threads.

Constructor of the Thread Class:

In the Thread class, there are four constructors available. Let's take a look at them, focusing on the ParameterizedThreadStart and ThreadStart delegates, and briefly mentioning the other two constructors.

Thread Class Constructors

  1. Thread(ThreadStart start):

    • Initializes a new instance of the Thread class, specifying a delegate that represents the method to be executed.
  2. Thread(ParameterizedThreadStart start):

    • Initializes a new instance of the Thread class, specifying a delegate that represents the method to be executed, and allowing an object to be passed to the method.
  3. Thread(ThreadStart start, int maxStackSize):

    • Initializes a new instance of the Thread class with a specified stack size.
  4. Thread(ParameterizedThreadStart start, int maxStackSize):

    • Initializes a new instance of the Thread class with a specified stack size, allowing an object to be passed to the method.

ThreadStart Delegate

ThreadStart is a delegate, which is a type-safe function pointer that can be used to call a method. It is similar to function pointers in C++ but provides type safety by ensuring that the delegate's signature matches the method's signature.

Why Type-Safe Function Pointer?

A delegate is called a type-safe function pointer because the signature of the delegate must exactly match the signature of the method it calls. This ensures that the correct method is invoked without runtime errors due to signature mismatches. Like in the previous example, the signature of Method1 and the delegate are the same. This way, we ensure that Method1 is the target method.

Example of ThreadStart

Let's discuss ThreadStart in detail:

If you goto the ThreadStart definition, you will see that it is a delegate defined in the System.Threading namespace.

In the previous example, the signature of the ThreadStart delegate and the signature of Method1 are the same: both have a void return type and no parameters.

Explanation of ThreadStart and Thread Initialization:-

When working with delegates, we follow three steps:

  1. Define a delegate (in this case, it's already defined as ThreadStart).

  2. Instantiate the delegate (binding the method to the delegate).

  3. Use the delegate.

Let's go through an example that demonstrates these steps.

Example Using ThreadStart Delegate:

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static void Method1()
        {
            Console.WriteLine("Hello world");
        }

        static void Main(string[] args)
        {
            // Step 2: Instantiate the delegate (bind the method to the delegate)
            ThreadStart ts = new ThreadStart(Method1); // The Method1 fulfills all criteria (bound to the delegate)

            // Create a new thread and pass the ThreadStart delegate instance
            Thread t = new Thread(ts); // Thread is taking ThreadStart delegate (delegate bound with Thread)

            // Start the thread
            t.Start();
        }
    }
}
// Output: Hello World

Difference Between the Two Examples:

Previous Example:

Thread T1 = new Thread(Method1);
T1.Start();
  • Implicit Delegate Binding: In this example, the Thread class constructor implicitly creates an instance of the ThreadStart delegate and binds it to Method1. The runtime and framework handle this implicitly.

Current Example:

ThreadStart ts = new ThreadStart(Method1);
Thread t = new Thread(ts);
t.Start();
  • Explicit Delegate Binding: In this example, we explicitly create an instance of the ThreadStart delegate and bind it to Method1. We then pass this delegate instance to the Thread constructor.

Clarification:

Thread t = new Thread(Method1); This line implicitly passes the method Method1 as a parameter to the constructor of the Thread class, which expects a ThreadStart delegate. The Common Language Runtime (CLR) and framework handle the creation of the ThreadStart delegate instance internally.


Q :Thread t = new Thread(Method1); In this constructor, we cannot pass a method directly, but here we pass the method directly. How?

Ans : The constructor Thread t = new Thread(Method1); does not directly take a method as a parameter. Instead, it implicitly or internally creates a ThreadStart delegate instance and binds it to the provided method. The CLR and framework handle this conversion internally.


These two code snippets are very much similar: Thread T1 = new Thread(Method1); and ThreadStart ts = new ThreadStart(Method1); Thread t = new Thread(ts); t.Start();

There are multiple ways to initialize and start a thread using the ThreadStart delegate in C#. Let's explore these different methods:

  1. Directly Pass the Method Name: This directly assigns the method Method1 to the ThreadStart delegate. The delegate automatically binds to the method that matches its signature.

  2. Using an Anonymous Method: This uses an anonymous method to bind the ThreadStart delegate to the logic of Method1. It allows you to write the method logic directly within the delegate if needed.

  3. Using a Lambda Expression: This uses a lambda expression to bind the ThreadStart delegate to Method1. Lambda expressions provide a concise way to represent anonymous methods.

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static void Method1()
        {
            Console.WriteLine("Hello world");
        }

        static void Main(string[] args)
        {
            // Option 1: Directly pass the method name
            // ThreadStart ts = Method1;

            // Option 2: Using an anonymous method
            // ThreadStart ts = delegate { Method1(); };

            // Option 3: Using a lambda expression
            ThreadStart ts = () => Method1();

            // Create a new thread and pass the ThreadStart delegate instance
            Thread t = new Thread(ts);

            // Start the thread
            t.Start();
        }
    }
}
// Output: Hello World
  • Option 1 is the simplest and most direct way to bind a method to a delegate.

  • Option 2 provides flexibility to include logic directly within the delegate.

  • Option 3 offers a concise and modern approach to delegate binding using lambda expressions.

In all the above cases, the method Method1 does not have any parameters. But what happens if Method1 has parameters? In that case, ThreadStart will not work. Instead, we use a parameterized thread class called ParameterizedThreadStart.

ParameterizedThreadStart Delegate

The ParameterizedThreadStart delegate is used to pass parameters to a thread method. This delegate takes an object as a parameter, which can then be cast to the appropriate type within the method.

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static void Method1(int i)
        {
            Console.WriteLine("Hello world " + i);
        }

        static void Method2(object num)
        {
            int i = Convert.ToInt32(num);
            Console.WriteLine("Hello world " + i);
        }

        static void Main(string[] args)
        {
            // Using ParameterizedThreadStart delegate to specify the method to be executed by the thread
            ParameterizedThreadStart ts = new ParameterizedThreadStart(Method2);

            // Create a new thread and pass the ParameterizedThreadStart delegate instance
            Thread t = new Thread(ts);

            // Start the thread and pass a parameter
            t.Start(50); // Output: Hello world 50

            // However, passing an incorrect type can cause runtime errors
            // The following line will cause a runtime error because Method2 expects an integer convertible object
            t.Start("Hello"); // This will result in a runtime exception (FormatException)

            // To make it type-safe, you can implement validation or use generic methods to ensure the correct type is passed
        }
    }
}

Key Points

  1. ParameterizedThreadStart Delegate:

    • This delegate allows passing a parameter to the thread method.

    • It takes an object as a parameter, which can be cast to the required type within the method.

  2. Example Usage:

    • The Method2 method takes an object parameter and converts it to an int.

    • The Thread class is instantiated with the ParameterizedThreadStart delegate.

    • The Start method of the thread is used to pass the parameter.

  3. Type Safety:

    • Since ParameterizedThreadStart takes an object, it is not type-safe by default.

    • Passing a wrong type, like a string when an integer is expected, will cause a runtime error (e.g., FormatException).

    • To ensure type safety, you can implement validation or use generic methods.

Join Method in Multitherading:

The Join method, available in the Thread class, is used to make the main thread wait until a specific thread finishes executing. This is useful to ensure that a particular thread completes its task before the calling thread continues.

  • Normal Thread Execution Without Join

      using System;
      using System.Threading;
    
      namespace Multithreading
      {
          internal class ThreadProgram
          {
              static void Method1()
              {
                  Console.WriteLine("Method 1 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 1: " + i);
                  }
                  Console.WriteLine("Method 1 thread end");
              }
              static void Method2()
              {
                  Console.WriteLine("Method 2 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 2: " + i);
                  }
                  Console.WriteLine("Method 2 thread end");
              }
              static void Method3()
              {
                  Console.WriteLine("Method 3 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 3: " + i);
                  }
                  Console.WriteLine("Method 3 thread end");
              }
              static void Main()
              {
                  Console.WriteLine("Main thread start");
                  Thread t1 = new Thread(Method1);
                  Thread t2 = new Thread(Method2);
                  Thread t3 = new Thread(Method3);
                  t1.Start();
                  t2.Start();
                  t3.Start();
                  Console.WriteLine("Main thread end");
              }
          }
      }
      /* Possible Output (varies each time):
      Main thread start
      Method 1 thread start
      Method 1: 0
      Method 2 thread start
      Method 2: 0
      Method 2: 1
      Method 2: 2
      Method 2: 3
      Method 2: 4
      Method 2 thread end
      Main thread end
      Method 1: 1
      Method 1: 2
      Method 1: 3
      Method 1: 4
      Method 1 thread end
      Method 3 thread start
      Method 3: 0
      Method 3: 1
      Method 3: 2
      Method 3: 3
      Method 3: 4
      Method 3 thread end
      */
    

    In this example, the main thread starts and then gives control to threads 1, 2, and 3. Then the main thread exits. The problem is that the main thread should exit in the middle of the program. I don't want the main thread to exit until all threads finish. That's why we use the join method.

  • UsingJointo Wait for Thread Completion: To ensure that the main thread waits for the other threads to complete before exiting, we use the Join method:

      using System;
      using System.Threading;
    
      namespace Multithreading
      {
          internal class ThreadProgram
          {
              static void Method1()
              {
                  Console.WriteLine("Method 1 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 1: " + i);
                  }
                  Console.WriteLine("Method 1 thread end");
              }
              static void Method2()
              {
                  Console.WriteLine("Method 2 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 2: " + i);
                  }
                  Console.WriteLine("Method 2 thread end");
              }
              static void Method3()
              {
                  Console.WriteLine("Method 3 thread start");
                  for (int i = 0; i < 5; i++)
                  {
                      Console.WriteLine("Method 3: " + i);
                  }
                  Console.WriteLine("Method 3 thread end");
              }
    
              static void Main()
              {
                  Console.WriteLine("Main thread start");
                  Thread t1 = new Thread(Method1);
                  Thread t2 = new Thread(Method2);
                  Thread t3 = new Thread(Method3);
                  t1.Start();
                  t2.Start();
                  t3.Start();
                  t1.Join(); // Wait for t1 to finish
                  t2.Join(); // Wait for t2 to finish
                  t3.Join(); // Wait for t3 to finish
                  Console.WriteLine("Main thread end");
              }
          }
      }
    
      /* Output:
      Main thread start
      Method 1 thread start
      Method 1: 0
      Method 1: 1
      Method 1: 2
      Method 1: 3
      Method 1: 4
      Method 1 thread end
      Method 2 thread start
      Method 2: 0
      Method 2: 1
      Method 2: 2
      Method 2: 3
      Method 2: 4
      Method 2 thread end
      Method 3 thread start
      Method 3: 0
      Method 3: 1
      Method 3: 2
      Method 3: 3
      Method 3: 4
      Method 3 thread end
      Main thread end
      */
    

    Here, the Join method ensures that the main thread does not exit until all the threads (t1, t2, and t3) have completed their execution. Generally, we use the Join method in the main method.

  • JoinMethod with Timeout: The Join method is overloaded and can accept a timeout value, allowing the calling thread to wait for a specified amount of time for the thread to complete:

      using System;
      using System.Threading;
    
      namespace Multithreading
      {
          internal class ThreadProgram
          {
              static void Method1()
              {
                  Console.WriteLine("Method 1 thread start");
                  for (int i = 0; i < 10; i++)
                  {
                      Console.WriteLine("Method 1: " + i);
                      Thread.Sleep(200);
                  }
                  Console.WriteLine("Method 1 thread end");
              }
              static void Method2()
              {
                  Console.WriteLine("Method 2 thread start");
                  for (int i = 0; i < 10; i++)
                  {
                      Console.WriteLine("Method 2: " + i);
                  }
                  Console.WriteLine("Method 2 thread end");
              }
              static void Method3()
              {
                  Console.WriteLine("Method 3 thread start");
                  for (int i = 0; i < 10; i++)
                  {
                      Console.WriteLine("Method 3: " + i);
                  }
                  Console.WriteLine("Method 3 thread end");
              }
    
              static void Main()
              {
                  Console.WriteLine("Main thread start");
                  Thread t1 = new Thread(Method1);
                  Thread t2 = new Thread(Method2);
                  Thread t3 = new Thread(Method3);
                  t1.Start(); t2.Start(); t3.Start();
                  t1.Join(1000); //1000 milliseconds: The main method waits for the t1 thread to finish in 1000 milliseconds. If t1 does not finish in 1000 milliseconds, the main thread will continue.
                  t2.Join(); t3.Join();
                  Console.WriteLine("Main thread end");
              }
          }
      }
      /*Out:-
      Main thread start
      Method 1 thread start
      Method 1: 0
      Method 2 thread start
      Method 2: 0
      Method 2: 1
      Method 2: 2
      Method 2: 3
      Method 2: 4
      Method 2: 5
      Method 2: 6
      Method 2: 7
      Method 2: 8
      Method 2: 9
      Method 2 thread end
      Method 3 thread start
      Method 3: 0
      Method 3: 1
      Method 3: 2
      Method 3: 3
      Method 3: 4
      Method 3: 5
      Method 3: 6
      Method 3: 7
      Method 3: 8
      Method 3: 9
      Method 3 thread end
      Method 1: 1
      Method 1: 2
      Method 1: 3
      Method 1: 4
      Main thread end
      Method 1: 5
      Method 1: 6
      Method 1: 7
      Method 1: 8
      Method 1: 9
      Method 1 thread end
      */
    

    If t1 does not complete within 1000 milliseconds, the main thread will continue its execution. However, since t2 and t3 do not have a timeout specified, the main thread will wait until they finish.

Summary about JOIN method:

  • The Join method is used to make the calling thread wait until the specified thread finishes its execution.

  • Using Join ensures that the main thread does not exit until other threads have completed, which is useful for synchronization.

  • The Join method can also accept a timeout value, allowing the calling thread to wait for a specified amount of time for the thread to complete. If the thread does not complete within this time, the calling thread continues its execution.

ThreadLocking:

In all the above examples, we used static methods. Now, we will use non-static methods:

Context Switching: When you run multiple threads in your code, the operating system shares time among each thread and transfers control between them. This process is called context switching.

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        public void Method1()
        {
            Console.Write("My name is ");
            Thread.Sleep(3000);
            Console.WriteLine("Mritunjay kumar");
        }

        static void Main()
        {

            //Calling Method1 three times using an instance of the class
            ThreadProgram obj = new ThreadProgram();
            //Output 1 come reason:-
            /*
            obj.Method1();
            obj.Method1();
            obj.Method1();
            */

            //Now use thread

            //Calling Method1 three times using threads (In real-world projects, more than one thread may need to access resources like a database)
            //Output 2 come reason:-           
            /*
            Thread t1 = new Thread(obj.Method1);//Bind the non-static method with thread.
            t1.Start();
            */

            //Output 3 come reason:-
            Thread t1 = new Thread(obj.Method1);
            Thread t2 = new Thread(obj.Method1);
            Thread t3 = new Thread(obj.Method1);
            t1.Start();
            t2.Start();
            t3.Start();
        }
    }
}
/*Output 1:-
My name is Mritunjay kumar
My name is Mritunjay kumar
My name is Mritunjay kumar
*/

/*Output 2:-
My name is Mritunjay kumar
*/

/*Output 3:-
My name is My name is My name is Mritunjay kumar
Mritunjay kumar
Mritunjay kumar
*/

In Output 3, the output appears this way because we use three threads. Each thread starts executing and prints the first statement, then they sleep. After sleeping, they print the next statement. Due to context switching controlled by the OS, the output gets mixed.

What happens if this method works with a database? It can cause big problems. So, how do we resolve this? To resolve this problem, we use Thread Locking mechanism.

Handling Context Switching with Thread Locking Mechanism:

Thread locking is a way to make sure that only one thread can access a critical part of the code or a shared resource at a time. This stops race conditions and keeps data consistent by making other threads wait until the lock is released.

using System;
using System.Threading;
namespace Multithreading
{
    internal class ThreadProgram
    {
        public void Method1()
        {
            lock (this)
            {
                Console.Write("My name is ");
                Thread.Sleep(3000);
                Console.WriteLine("Mritunjay kumar");
            }
        }

        static void Main()
        {
            ThreadProgram obj = new ThreadProgram();

            Thread t1 = new Thread(obj.Method1);
            Thread t2 = new Thread(obj.Method1);
            Thread t3 = new Thread(obj.Method1);

            t1.Start();
            t2.Start();
            t3.Start();
        }
    }
}
/*Out:-
My name is Mritunjay kumar
My name is Mritunjay kumar
My name is Mritunjay kumar
*/

Locking:

  • The lock statement ensures that only one thread can enter the critical section of the code at a time.

  • When a thread enters the lock statement, it acquires a lock on lockObj, stopping other threads from entering the locked section until the lock is released.

  • Example: Imagine three people using the same glass to drink water. When the first person drinks, the others wait. Once the first person finishes and leaves, the next person can drink. It means access is given to one thread at a time.

Note'

  • Context Switching: The OS shares CPU time among threads, leading to interleaved execution.

  • Thread Locking: Using lock ensures that only one thread accesses the critical section at a time, preventing issues with shared resources.

  • Join Method: The Join method ensures that the calling thread waits for another thread to complete its execution.

Thread Priority:

What happens if one thread has more work to do compared to another thread? In that case, we can set the thread priority. Let's see how:

When one thread has more work to do compared to another thread, we can set the thread priority to manage CPU resource allocation more efficiently. The Priority property in the Thread class allows us to set the priority of a thread. This priority is defined under the ThreadPriority enum, which has five levels:

  1. Lowest

  2. BelowNormal

  3. Normal

  4. AboveNormal

  5. Highest

By default, every thread runs with a Normal priority. According to the levels, the CPU resources used increase: Lowest uses the least CPU resources, while Highest uses the most.

Example with Same Priority:

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static long count1, count2;
        static void Method1()
        {
            while (true) 
            {
                count1 += 1;
            }
        }
        static void Method2()
        {
            while (true)
            {
                count2 += 1;
            }
        }

        static void Main()
        {
            Thread t1 = new Thread(Method1);
            Thread t2 = new Thread(Method2);

            t1.Start();
            t2.Start();

            Thread.Sleep(10000); // Main thread sleeps for 10 seconds

            // Terminate the threads
            t1.Abort();
            t2.Abort();

            // Wait until the threads are fully terminated
            t1.Join();
            t2.Join();

            Console.WriteLine("Count1: " + count1);
            Console.WriteLine("Count2: " + count2);
        }
    }
}

/* Output:
Count1: 1170866865
Count2: 1171613038
*/
// Each time you get a different result.

In this example, both methods execute with the same priority (Normal), so the results should be similar, but small differences will occur due to the operating system's scheduling. The OS decides which thread to run at any given time, resulting in varying counts for each run.

Thread priority only sets the order of importance, not the exact amount of CPU time a thread will receive.

Example with diffrence Priority:

using System;
using System.Threading;

namespace Multithreading
{
    internal class ThreadProgram
    {
        static long count1, count2;
        static void Method1()
        {
            while (true)
            {
                count1 += 1;
            }
        }
        static void Method2()
        {
            while (true)
            {
                count2 += 1;
            }
        }

        static void Main()
        {
            Thread t1 = new Thread(Method1);
            Thread t2 = new Thread(Method2);

            // Setting the priority
            t1.Priority = ThreadPriority.Lowest;
            t2.Priority = ThreadPriority.Highest;

            t1.Start();
            t2.Start();

            Thread.Sleep(10000); // Main thread sleeps for 10 seconds

            // Terminate the threads
            t1.Abort();
            t2.Abort();

            // Wait until the threads are fully terminated
            t1.Join();
            t2.Join();

            Console.WriteLine("Count1: " + count1);
            Console.WriteLine("Count2: " + count2);
        }
    }
}

/* Output:
Count1: 343970565
Count2: 1349243933
*/

By setting the priority of t1 and t2 threads, the t1 thread uses less CPU resources, and the t2 thread uses more CPU resources. That way, the Count1 value is lower, and the Count2 value is higher.

Difference between single-threaded and multi-threaded model:

using System;
using System.Threading;
using System.Diagnostics; //Use to measurement of the time

namespace Multithreading
{
    internal class ThreadProgram
    {
        static long count1, count2;
        static void Method1()
        {
            for (int i = 0; i <= 10000000; i++) 
            {
                count1 += 1;
            };
        }
        static void Method2()
        {
            for (int i = 0; i <= 10000000; i++)
            {
                count2 += 1;
            };
        }

        static void Main()
        {
            Stopwatch StopWatchByThread = new Stopwatch();
            Stopwatch StopWatchWithoutThread = new Stopwatch();

            Thread t1 = new Thread(Method1);
            Thread t2 = new Thread(Method2);

            StopWatchByThread.Start();
            t1.Start(); t2.Start();
            Console.WriteLine("Stop Watch By Thread: "+ StopWatchByThread.ElapsedMilliseconds + " Milliseconds");

            StopWatchWithoutThread.Start();
            Method1(); Method2();
            Console.WriteLine("Stop Watch Without Thread: " + StopWatchByThread.ElapsedMilliseconds + " Milliseconds");
        }
    }
}

/* Output:
Stop Watch By Thread: 9 Milliseconds
Stop Watch Without Thread: 224 Milliseconds
*/
// Each time you get a different result.

In this example, Method1(); Method2(); uses only the main method thread, while t1.Start(); t2.Start(); uses multiple threads.


Collections:

Definition: A collection is a dynamic array that can automatically resize itself and manage its elements, providing greater flexibility compared to a traditional array.

Arrays vs. Collections:

  • Arrays:

    • Arrays have a fixed size. Once declared, their size cannot be changed.

    • You can resize an array using Array.Resize, but this creates a new array with the new size and destroys the old one.

Example: Resizing an Array

internal class Program
{
    static void Main(string[] args)
    {
        int[] arr = new int[10];
        Array.Resize(ref arr, 12); // This uses an output parameter (ref)
    }
}

In this example, the old array is destroyed, and a new array with a size of 12 is created.

Limitations of Arrays:

  • The size of an array cannot be increased directly.

  • You cannot add a value in the middle of an array if it already contains values.

  • To add a new value, you need to increase the array's size, which involves creating a new array.

  • Similarly, you cannot delete a value in the middle of an array directly.

Collections:

  • Automatically increase its size when new values are added.

  • Insert and delete values in the middle of the collection.

Collections in .NET:

  1. Non-Generic Collections

  2. Generic Collections

STACK:

  • Stack stores values as LIFO (Last In First Out). It has a Push() method to add a value and Pop() & Peek() methods to get values.

  • Stack is a special collection that stores elements in LIFO style. C# has both generic and non-generic Stack classes. It's better to use the generic Stack collection.

  • Stack is useful for storing temporary data in LIFO style, and you might want to delete an element after getting its value.

Creating a stack:

Stack<int> myStack = new Stack<int>();
myStack.Push(1);
myStack.Push(2);
myStack.Push(3);
myStack.Push(4);

foreach (var item in myStack)
    Console.Write(item + ","); //prints 4,3,2,1,

QUEUE:

  • Queue stores values in FIFO style (First In First Out). It keeps the order in which values were added.

  • It provides an Enqueue() method to add values and a Dequeue() method to get values from the collection.

  • Queue is a FIFO (First In First Out) collection.

  • It is part of the System.Collections.Generic namespace.

  • Queue can hold elements of a specified type. It provides compile-time type checking and avoids boxing-unboxing because it is generic.

  • Elements can be added using the Enqueue() method. You cannot use collection-initializer syntax.

  • Elements can be retrieved using the Dequeue() and Peek() methods. It does not support indexing.

Creating a Queue:

Queue<int> callerIds = new Queue<int>();
callerIds.Enqueue(1);
callerIds.Enqueue(2);
callerIds.Enqueue(3);
callerIds.Enqueue(4);

foreach(var id in callerIds)
    Console.Write(id); //prints 1234

We do not use angular brackets <> when creating a non-generic collection. Example:

Generic collection:

ArrayList<int> arl=new ArrayList<int> ();

Non-Generic collection:

ArrayList arl=new ArrayList();

Non-Generic Collections:

  • Collections were introduced in .NET 1.0 as non-generic collections.

  • Examples of non-generic collections include Stack, Queue, LinkedList, SortedList, ArrayList, and Hashtable.

  • These collections are implemented as classes and are defined in the System.Collections namespace.

In traditional programming languages like C++, you need to define and implement Stack, Queue, LinkedList, SortedList, ArrayList, and Hashtable data structures yourself. However, in .NET, they are provided as part of the framework in the form of classes within the System.Collections namespace.

Summary

  • Arrays: Have fixed size, cannot be resized directly, and have limitations in inserting and deleting values.

  • Collections: Provide dynamic resizing, and the ability to insert and delete values easily.

  • Non-Generic Collections: Introduced in .NET 1.0, they include Stack, Queue, LinkedList, SortedList, ArrayList, and Hashtable, all defined in the System.Collections namespace.

Q. Diffrence bitween Array and ArrayList?

Ans:-

ArrayArrayList
Fixed LengthVariable Length
Not possible to insert itemsWe can insert item into the middle
Not posible to delete itemsWe can delete items from the middle

ArrayList:

Using an ArrayList is similar to using an array:

Definition: An ArrayList is a dynamic array that can automatically resize to fit new elements. Unlike traditional arrays, the size of an ArrayList changes as elements are added or removed.

To use ArrayList, you need to import the System.Collections namespace:

Basic Example of UsingArrayList:

using System;
using System.Collections;

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(); // Size dynamically increases as needed
            al.Add(100); // Store any type of value in ArrayList at the last position
            Console.WriteLine("Element added: " + al[0]);
        }
   }
}

al.Add(100) stores a value in the first cell. You might wonder how many cells are in the ArrayList. To understand this, we need to know about the property called capacity. The capacity is a property that tells us the number of items that can be stored in a collection.

Understanding Capacity:

The capacity of an ArrayList is the number of elements it can hold before it needs to resize. The initial capacity can grow automatically as you add more elements.

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(); // Size dynamically increases as required
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: Initial Capacity: 0
            al.Add(100); // Add an element
            Console.WriteLine("Capacity after adding one element: " + al.Capacity); // Output: Capacity after adding one element: 4
        }
   }
}

When the initial capacity is filled, the ArrayList doubles its capacity.

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(); // Size dynamically increases as required
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: Initial Capacity: 0
            al.Add(100); al.Add(200); al.Add(300); al.Add(400);
            Console.WriteLine("Capacity after adding four elements: " + al.Capacity); // Output: Capacity after adding four elements: 4    
            al.Add(500);
            Console.WriteLine("Capacity after adding fifth element: " + al.Capacity); // Output: Capacity after adding fifth element: 8
        }
   }
}

If the 4 items fill the capacity, it becomes 8. If 8 is filled, the capacity changes to 16. If 16 is filled, the capacity changes to 32. This is how the capacity increases.

The special feature of ArrayList is that it resizes automatically. There is no limit.

Another constructor in ArrayList is the parameterized constructor. You can also specify the size. If you pass the size, it means you can set the initial capacity.

Parameterized Constructor:

You can also initialize an ArrayList with a specified initial capacity:

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(5); // Set initial capacity to 5
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: Initial Capacity: 5
            al.Add(100); al.Add(200); al.Add(300); al.Add(400); al.Add(500);
            Console.WriteLine("Capacity after adding five elements: " + al.Capacity); // Output: Capacity after adding five elements: 5
            al.Add(600);
            Console.WriteLine("Capacity after adding sixth element: " + al.Capacity); // Output: Capacity after adding sixth element: 10
        }
   }
}

Operations onArrayList:

  • Adding Elements:
al.Add(100); // Adds 100 to the end of the ArrayList
  • Inserting Elements:
al.Insert(3, 350); // Inserts 350 at index 3
  • Removing Elements:
al.Remove(350); // Removes the first occurrence of 350
al.RemoveAt(3); // Removes the element at index 3
  • Printing Elements:
foreach (object o in al)
{
    Console.Write(o + " "); // Prints all elements in the ArrayList
}
Console.WriteLine();

Complete Example:

using System;
using System.Collections;

namespace Collection
{
   internal class Program
   {
       static void Main(string[] args)
       {
            ArrayList al = new ArrayList(5); // Set initial capacity to 5
            Console.WriteLine("Initial Capacity: " + al.Capacity); // Output: 5

            al.Add(100); al.Add(200); al.Add(300); al.Add(400); al.Add(500);
            Console.WriteLine("Capacity after adding five elements: " + al.Capacity); // Output: 5

            al.Add(600);
            Console.WriteLine("Capacity after adding sixth element: " + al.Capacity); // Output: 10

            // Print all elements
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 400 500 600
            }
            Console.WriteLine();

            // Insert new item in the middle
            al.Insert(3, 350);
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 350 400 500 600
            }
            Console.WriteLine();

            // Remove the item
            al.Remove(350);
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 400 500 600
            }
            Console.WriteLine();

            // Remove by index position
            al.RemoveAt(3);
            foreach (object o in al)
            {
                Console.Write(o + " "); // Output: 100 200 300 500 600
            }
            Console.WriteLine();
        }
   }
}

This corrected explanation and examples should help you understand how to use ArrayList in .NET effectively.

Hashtable:

When working with arrays and ArrayList, you access elements using an index. This can be limiting when dealing with more complex data, like storing employee information (name, salary, position). In these cases, managing or remembering indexes can be difficult.

Problem with Arrays and ArrayLists:

  • Arrays and ArrayList use predefined indexes that cannot be changed, making it difficult to remember and manage the position of each element.

  • For example, if you store employee data, it's tough to remember which index corresponds to which information (e.g., name, salary).

Solution: Hashtable

A Hashtable addresses this issue by storing data in key-value pairs. This means you can define keys to make the data more readable and accessible.

Key Features of Hashtable:

  • Dynamic Size: Automatically resizes as needed. Similar to ArrayList, it can resize itself, and you can insert or remove values in the middle.

  • Key-Value Pairs: Allows for storing data with user-defined keys, making it easier to access and manage data.

Example Usage:

using System;
using System.Collections;

namespace Collection
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Create a Hashtable instance
            Hashtable employeeData = new Hashtable();

            // Add key-value pairs to the Hashtable
            employeeData.Add("FName", "Mritunjay"); // Using the Add method
            employeeData["LName"] = "Kumar"; // Using the indexer
            employeeData["Salary"] = 50000;
            employeeData["Position"] = "Software Developer";

            // Access and print the values using keys
            Console.WriteLine("Name: " + employeeData["FName"] + " " + employeeData["LName"]); // Output: Name: Mritunjay Kumar
            Console.WriteLine("Salary: " + employeeData["Salary"]); // Output: Salary: 50000
            Console.WriteLine("Position: " + employeeData["Position"]); // Output: Position: Software Developer

            // Get all the keys from the Hashtable
            foreach (object key in employeeData.Keys)
            {
                Console.Write(key + " ");
            } // Output: FName LName Salary Position (Order is not same alwase order may vary due to hashing)
        }
    }
}

In .NET, every class by default contains four methods: GetHashCode(), Equals(), GetType(), and ToString().

What is a Hashcode?

  • The GetHashCode() method returns a numeric representation of an object, known as a hash code. For example, if you write Console.WriteLine("Name".GetHashCode());, it might return a numeric value like -694638. This numeric value is called a hash code.

  • Hash codes are consistent for the same value. For instance, calling GetHashCode() on the string "Name" will always return the same hash code.

Hashcode in Hashtable:

  • In a Hashtable, every item contains a key, a value, and a hash code. The key and value are objects, while the hash code is an integer.

  • The Hashtable uses the hash code of the key to quickly locate the corresponding value, making data retrieval very efficient.

Why is Fetching Data Using Hashcode Efficient?

  • Hash codes allow for efficient data retrieval. Instead of searching through an array or ArrayList by index, a Hashtable can use the hash code to directly access the desired value.

  • This is why Hashtable uses hash codes, as they enable fast and efficient data access compared to using indices in arrays or ArrayList.

Advantages of Hashtable:

  • Readability: Keys make the data more understandable (e.g., "Name" instead of an index).

  • Ease of Access: You can directly access data using keys without remembering indices.

  • Flexibility: Easily add, update, or remove key-value pairs as needed.

By using a Hashtable, you can store and manage complex data more efficiently compared to arrays and ArrayList.

Drawback of Non-Generic Collections

One of the main drawbacks of using non-generic collections like ArrayList and Hashtable is that they are not type-safe. This is because these collections store elements as objects, allowing any type of value to be added. For example:

internal class Program
{
    static void Main(string[] args)
    {
        ArrayList al = new ArrayList();

        al.Add(200);
        al.Add("Mritunjay");
        al.Add(true);

        Console.WriteLine();
    }
}

In the above example, the ArrayList accepts all types of values (integer, string, boolean), which can lead to runtime errors and type casting issues.

Problem with Non-Generic Collections:

  • Lack of Type Safety: Since non-generic collections store values as objects, any type of value can be added. This can lead to runtime errors when the wrong type is retrieved or cast.

  • Potential Performance Issues: Storing values as objects requires boxing and unboxing for value types, which can degrade performance.

  • Difficult to Manage: When dealing with large collections of specific types, managing and ensuring the correct type can be cumbersome and error-prone.

Solution: Generic Collections

Generic Collections:

To overcome these limitations, .NET 2.0 introduced generic collections. Generic collections provide both strongly type safety and automatic resizing.

What is a Generic Collection?

A generic collection is a strongly-type-safe collection that allows you to specify the type of elements it can store. This ensures that only the specified type can be added to the collection, preventing runtime errors and improving performance by eliminating the need for boxing and unboxing.

How Type Safety Works in Generic Collections:

In generic collections, type safety is ensured by using type parameters. When you create a generic collection, you specify the type of elements it will store with the syntax List<T>, where T is the type. This ensures the collection can only store elements of that specified type.

Example of Type Safety in a Generic Collection

When you define a generic list with a specific type, the collection is restricted to store only that type of elements. For instance, List<int> can only store integers. Attempting to store a different type will result in a compile-time error.

using System;
using System.Collections.Generic;

namespace CollectionExample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Creating a generic list of integers
            List<int> intList = new List<int>();

            // Adding integer values to the list
            intList.Add(200);
            intList.Add(300);
            intList.Add(400);

            // This line would cause a compile-time error because the list is type-safe
            // intList.Add("Mritunjay");

            // Displaying the values
            foreach (int value in intList)
            {
                Console.WriteLine(value);
            }
        }
    }
}

In the example above, intList is a list that only accepts integers. If you try to add a string or any other type, the compiler will throw an error.

Using Complex Types in Generic Collections:

Generic collections are not limited to simple types like integers or strings. You can also use complex types, such as custom classes, as type parameters.

using System;
using System.Collections.Generic;

namespace CollectionExample
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public double Balance { get; set; }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            // Creating a generic list of Customer objects
            List<Customer> customers = new List<Customer>();

            // Adding Customer objects to the list
            customers.Add(new Customer { Id = 1, Name = "John Doe", Balance = 1000.50 });
            customers.Add(new Customer { Id = 2, Name = "Jane Smith", Balance = 2000.75 });

            // Displaying the customer details
            foreach (Customer customer in customers)
            {
                Console.WriteLine($"Id: {customer.Id}, Name: {customer.Name}, Balance: {customer.Balance}");
            }
        }
    }
}

In the example above, customers is a list that only accepts Customer objects. This ensures type safety and makes the code more readable and maintainable.

We will discuss this line customers.Add(new Customer { Id = 1, Name = "John Doe", Balance = 1000.50 }); later.

System.Collections vs. System.Collections.Generic:

  • System.Collections: Contains non-generic collections like ArrayList and Hashtable. These collections can store any type of objects but lack type safety.

  • System.Collections.Generic: Contains generic collections like List<T>, Dictionary<TKey, TValue>, Queue<T>, and Stack<T>. These collections provide type safety and eliminate the need for type casting.

Using generic collections, you can specify the type of elements the collection should store, ensuring that only that type is allowed. This enhances type safety and reduces the likelihood of runtime errors caused by incorrect type handling.

Using Generic Lists in Code:-

To use a generic list in your code, specify the type parameter when creating an instance of the List class. This ensures that only the specified type can be stored in the list.

using System;
using System.Collections.Generic;

namespace Collection
{
    class GenericList
    {
        static void Main()
        {
            List<int> list = new List<int>();//The bhebhier of this list class is exactly same as the behaviour of ArrayList in collection but diffrence is ArrayList store any type of value but List can store specific type of value

            list.Add(10); list.Add(20); list.Add(30);
            //list.Add(60.5)//Error come

            list.Insert(2, 25);

            list.Remove(2);

            foreach (object o in list)
            {
                Console.Write(o + " ");
            }
        }
    }
}

By using generic collections, you can leverage the benefits of type safety and automatic resizing, making your code more robust and easier to maintain.

Generics:

Earlier, we learned that in the ArrayList, the Add method accepts an Object, but in List<T>, the Add method only accepts a particular type of value.

In C# 2.0, Microsoft introduced generics to address the limitations of collections like ArrayList and methods that handle different data types. Generics improve type safety and performance by allowing you to define classes, methods, and interfaces with a placeholder for the type of data they store or use.

Problems with Non-Generic Code:

using System;

namespace Collection
{
    internal class GenericTest
    {
        public bool Compare(int a, int b)
        {
            return a == b;
        }

        public bool Compare(float a, float b)
        {
            return a == b;
        }

        public bool Compare(object a, object b)
        {
            return a.Equals(b);
        }

        static void Main()
        {
            GenericTest obj = new GenericTest();

            // Compare int
            bool result = obj.Compare(10, 10);
            Console.WriteLine(result); // Output: True

            // Compare float
            result = obj.Compare(10.56f, 10.56f);
            Console.WriteLine(result); // Output: True

            // Compare object
            result = obj.Compare(10, 10);
            Console.WriteLine(result); // Output: True

            // Compare bool
            result = obj.Compare(true, true);
            Console.WriteLine(result); // Output: True

            // Problem: Compare float and double
            result = obj.Compare(10.56f, 10.56);
            Console.WriteLine(result); // Output: False
        }
    }
}

In the code above, there are two main problems we see in result = obj.Compare(10.56f, 10.56);:

  1. Lack of Type Safety: The Compare method that takes object parameters can compare any types without type checking, leading to potential errors at runtime.

  2. Performance Issues: When we pass a value which is a value type (float and double) and the Compare method takes an object, which is a reference type, it internally performs boxing to convert the value type to a reference type. Because of this, the boxing operation is performed internally. If we want to use a float value and a double value, we have to unbox them again. So, every time boxing and unboxing occur when we use this type of function, the performance of our program decreases.

To solve this problem, Microsoft introduced Generics in C# 2.0. With Generics, you can make methods type-safe and avoid using boxing and unboxing.

Example of Generic method:-

using System;

namespace Collection
{
    internal class GenericTest
    {
        public bool Compare<T>(T a, T b)
        {
            return a.Equals(b);
        }

        static void Main()
        {
            GenericTest obj = new GenericTest();

            // Compare float values
            bool result = obj.Compare<float>(10.56f, 10.56f);
            Console.WriteLine(result); // Output: True
        }
    }
}

Explanation:

  1. Generic Method Definition:

     public bool Compare<T>(T a, T b)
     {
         return a.Equals(b);
     }
    
    • The method Compare is defined with a type parameter T. This means the method can accept arguments of any type, as long as both arguments are of the same type.
  2. Calling the Generic Method with Explicit Type Parameter:

     GenericTest obj = new GenericTest();
     bool result = obj.Compare<float>(10.56f, 10.56f);
     Console.WriteLine(result); // Output: True
    
    • Here, the Compare method is explicitly called with float as the type parameter. This ensures that T is set to float .
  3. Additional Examples with Different Types:

    • Compare int values: result =obj.Compare<int>(10, 10);

    • Compare string values: result =obj.Compare<string>("Hello", "Hello");

  4. Type Safety:

    • The generic Compare method ensures type safety. If you try to pass arguments of different types, the compiler will throw an error. For example:

        // This will cause a compile-time error
        bool result = obj.Compare(10.56f, 10.56);
      
    • In this case, one argument is a float and the other is a double. The compiler will not allow this because it expects both arguments to be of the same type.

  5. Avoiding Boxing and Unboxing:

    • Generics eliminate the need for boxing and unboxing. In non-generic collections or methods that use object, value types are boxed (converted to object) when added to the collection or passed as arguments, and unboxed (converted back to the original type) when retrieved. This can negatively impact performance.

    • With generics, the actual type is used, avoiding the overhead of boxing and unboxing, thus improving performance

Using Generics:

Generics in C# allow for the creation of flexible, type-safe methods and classes, eliminating the need for boxing and unboxing, and improving performance. Let's explore how to use generics with methods and classes to perform.

Generic Methods:

Generics ensure type safety, but they don't directly support arithmetic operations because the type T is unknown at compile time. To solve this, we can use the dynamic type, which allows the type to be resolved at runtime.

using System;

namespace Collection
{
    class GenericTest
    {
        public void Add<T>(T a, T b)
        {
            // Console.WriteLine(a + b); // This line would produce an error because at compile time, the types of 'a' and 'b' are unknown. To perform operations like addition, we need to use the 'dynamic' type to resolve the types at runtime.
            dynamic d1 = a; 
            dynamic d2 = b;
            Console.WriteLine(d1 + d2);
        }
        public void Sub<T>(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 - d2);
        }
        public void Mul<T>(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 * d2);
        }
        public void Div<T>(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 / d2);
        }
        static void Main()
        {
            GenericTest obj = new GenericTest();
            obj.Add<int>(10, 20);
            obj.Sub<int>(10, 20);
            obj.Mul<int>(10, 20);
            obj.Div<int>(10, 20);
        }
    }
}

In the above example, dynamic allows the Add, Sub, Mul, and Div methods to work with various data types at runtime, performing automatic type conversion. For instance, if a and b are integers, d1 and d2 will be treated as integers; if they are floats, d1 and d2 will be treated as floats, and so on.

Dynamic: dynamic is a new feature introduced in C# 4.0 that allows declaring a variable as dynamic, meaning its data type is identified at runtime. In C# 3.0, we have var, which is similar to dynamic, but with var, the data type is identified at compile time (var identifies the data type at compile time, while dynamic identifies it at runtime). At runtime, if you pass an int value to a and b, then d1 and d2 become int. If you pass a float value, then d1 and d2 become float. If you pass a decimal value, they automatically convert to decimal. Automatic conversion happens.

Understandingvaranddynamic:-

In C#, the dynamic type was introduced in version 4.0, allowing you to declare variables whose type is determined at runtime. This is different from var, introduced in C# 3.0, which determines the type at compile time.

Key Differences Betweenvaranddynamic:

  • var:

    • Determines the data type at compile time.

    • The compiler figures out the type from the assigned value.

    • Once assigned, the type cannot change.

  • dynamic:

    • Determines the data type at runtime.

    • Allows more flexible code, as the type can change based on the assigned value.

    • Useful when the type is not known until runtime.

Summary:

  • var: Used when the type is known at compile time. The type is inferred from the assigned value and cannot change.

  • dynamic: Used when the type is not known until runtime. It allows the type to change based on the assigned value, providing greater flexibility for dynamic scenarios.

Generic Classes with Dynamic Typing:

We can also use generics at the class level, allowing us to specify the type once when creating an instance of the class. This avoids having to specify the type with each method call.

using System;

namespace Collection
{
    class GenericTest2<T>
    {
        public void Add(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 + d2);
        }

        public void Sub(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 - d2);
        }

        public void Mul(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 * d2);
        }

        public void Div(T a, T b)
        {
            dynamic d1 = a;
            dynamic d2 = b;
            Console.WriteLine(d1 / d2);
        }
    }

    class Program
    {
        static void Main()
        {
            GenericTest2<int> obj = new GenericTest2<int>();
            obj.Add(10, 20);
            obj.Sub(10, 20);
            obj.Mul(10, 20);
            obj.Div(10, 20);
        }
    }
}

The advantage here is that you don't need to specify <T> for each method; you specify at once when defining the class. This way, all methods in the class use the same type, making the code simpler and easier to read.

Summary:

Just as we use generics with collections like List<T>, we can apply the same concept to our own classes. For example:

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

We can also create an instance of a generic class:

GenericTest2<int> obj = new GenericTest2<int>();

In this case, because int is specified, all methods in GenericTest2 will operate on integers. If we want to use floats, we can do:

GenericTest2<float> obj = new GenericTest2<float>();

Then all methods will operate on floats. The same applies if we use double:

GenericTest2<double> obj = new GenericTest2<double>();

Whatever type is specified, all methods in the class will operate on that type. This pattern shows how generic collections and classes are implemented in .NET 2.0. The use of generics provides type safety, flexibility, and performance benefits, ensuring that all methods operate on the specified type and eliminating the need for type casting.

Using Dictionaries and Lists in Generic Collections:

In the previous lesson, we learned about the Hashtable, which stores key-value pairs. In generic collections, the Hashtable is replaced with the Dictionary. When creating a Dictionary, it takes two types: the first is KeyType and the second is ValueType. This is denoted as Dictionary<TKey, TValue>. A List takes only one type, which is denoted as List<T>.

Example:

using System;
using System.Collections.Generic;

namespace Collection
{
    class DictionaryTest
    {
        static void Main()
        {
            Dictionary<string, object> dt = new Dictionary<string, object>();
            // 'string' represents the key and 'object' represents the value

            // Insert values
            dt.Add("Name", "Mritunjay Kumar");
            dt.Add("Job", "Manager");
            dt.Add("Salary", 25000.00);
            dt.Add("Age", 23);
            dt.Add("Gender", 'M');

            // Retrieve values
            foreach (string key in dt.Keys)
            {
                Console.WriteLine(key + ": " + dt[key]);
            }
        }
    }
}

Dictionaries store values in a sequence, whereas Hashtables do not store values in a sequence.

In the case of a generic collection, the type of values we want to store in the collections need not be predefined types only (like int, float, char, string, bool, etc.). It can also be some user-defined type.

Example with a User-Defined Type:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // In a list, we can do the following:
            List<Customer> list = new List<Customer>();

            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };

            // Add all instances to the list
            list.Add(c1);
            list.Add(c2);
            list.Add(c3);

            // Or we can store all three values at once
            // list.AddRange(new Customer[] { c1, c2, c3 });

            // Retrieve all values
            foreach (Customer obj in list)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }

            // Output:
            /*
             101 Mritunjay Hyderabad 25000
             102 Sumit Delhi 24000
             103 Rahul Chennai 21000
             */
        }
    }
}

In a list, we can store not only predefined values but also user-defined values.

There are two interfaces generally used in collections:

  • IComparable interface

  • IComparer interface

Example:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            // Assign all instances to a list
            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Retrieve all values
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
//Out:-
/*
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
*/

To sort the list, we use the Sort() method like that:

using System;
using System.Collections.Generic;

namespace Collection
{
    class GenericList
    {
        static void Main()
        {
            List<int> list = new List<int>();//The bhebhier of this list class is exactly same as the behaviour of ArrayList in collection but diffrence is ArrayList store any type of value but List can store specific type of value

            list.Add(100); list.Add(40); list.Add(50);
            list.Add(90); list.Add(30); list.Add(24);
            foreach (object o in list)
            {
                Console.Write(o + " ");
            }
            Console.WriteLine();

            list.Sort();

            foreach (object o in list)
            {
                Console.Write(o + " ");
            }
            Console.ReadLine();
        }
    }
}
//Out:-
/*
100 40 50 90 30 24
24 30 40 50 90 100
 */

But, if you use the Sort() method with List<Customer>, you will get an error. This happens because Customer is a complex type and the compiler is unsure of how to sort it (by CustId, Name, City, or Balance). To sort the data, we need to write the sorting logic inside the Customer class using the IComparable<Customer> interface and its CompareTo method. And i also want to short by balance or any other value.

Short by balance:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sort based on Balance 
            if (this.Balance > other.Balance)
            {
                return 1; // If this instance's Balance is greater
            }
            else if (this.Balance < other.Balance)
            {
                return -1; // If this instance's Balance is less
            }
            else
            {
                return 0; // If both Balance are equal
            }
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            // Assign all instances to a list
            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Sort the list
            cus.Sort();

            // Retrieve all values after sorting
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
//Out:-
/*
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
102 Sumit Delhi 24000
101 Mritunjay Hyderabad 25000
 */

If you want to reverse the sort order, you only need to change the return values in the CompareTo method:

Reverse using CustId:

public int CompareTo(Customer other)
{
    if (this.CustId > other.CustId)
    {
        return -1; // Reverse the comparison
    }
    else if (this.CustId < other.CustId)
    {
        return 1; // Reverse the comparison
    }
    else
    {
        return 0;
    }
}
//Out:-
/*
106 Gudu Uttar Pradesh 23800
105 Mohan Jharkhand 21800
104 Amit Delhi 21500
103 Rahul Chennai 21000
102 Sumit Delhi 24000
101 Mritunjay Hyderabad 25000
*/

But assume you don't have access to the Customer class code and it sorts data based on CustId, but you want to sort based on Balance. In this case, you can create a new class that implements IComparer<Customer> to achieve this. Let's see how:

Example with Custom Sorting:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)
            {
                return 1;
            }
            else if (this.CustId < other.CustId)
            {
                return -1;
            }
            else
            {
                return 0;
            }
        }
    }

    class BalanceComparer : IComparer<Customer>
    {
        public int Compare(Customer x, Customer y) //These parameters represent the two Customer objects that need to be compared. 
        {
            // Sorting by Balance
            if (x.Balance > y.Balance)
            {
                return 1;
            }
            else if (x.Balance < y.Balance)
            {
                return -1;
            }
            else
            {
                return 0;
            }
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            // Create instances of Customer
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            // Assign all instances to a list
            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Retrieve all values sorted by CustId
            Console.WriteLine("Sorted by CustId:");
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }

            Console.WriteLine();

            BalanceComparer balanceComparer = new BalanceComparer();
            cus.Sort(balanceComparer);

            // Retrieve all values sorted by Balance
            Console.WriteLine("Sorted by Balance:");
            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
Sorted by CustId:
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800

Sorted by Balance:
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
102 Sumit Delhi 24000
101 Mritunjay Hyderabad 25000

The Sort() method can work in two ways:

  1. Sort() without parameters: This uses the default way of sorting provided in the Customer class, which sorts by CustId because it implements the IComparable interface.

  2. Sort(IComparer<T>) with a custom comparer: This uses a custom way of sorting, like sorting by Balance, provided by an IComparer implementation. In our example, we use the BalanceComparer class for this. IComparer is a parameter.

This makes sorting flexible, allowing you to sort Customer objects by different properties depending on what you need.

Now we have two options: Built-in Sorting and Custom Sorting.

Sort() Method Overloads:

The Sort() method has four overloads:

  1. Sort() without parameters:

    • Uses the default way of sorting provided in the class, such as Customer, which sorts by CustId because it implements the IComparable interface. And this is also suitable for simple types like int, float, double, etc. Alwarady seen.
  2. Sort(Comparison<T> comparison):

    • Takes a Comparison<T> delegate as a parameter. This delegate represents the method that compares two objects of the same type. It's useful for defining custom sorting logic directly in place.
  3. Sort(IComparer<T> comparer):

    • Uses can short custom way, like sorting by Balance, provided by an IComparer<T> implementation. In our example, we use the BalanceComparer class for this. IComparer is a parameter. Alwarady seen.
  4. Sort(int index, int count, IComparer<T> comparer):

    • Sorts a range of elements in the list using the specified comparer. This allows you to specify which portion of the list to sort.

    • Example:

        using System;
        using System.Collections.Generic;
      
        namespace Collection
        {
            public class Customer : IComparable<Customer>
            {
                public int CustId { get; set; }
                public string Name { get; set; }
                public string City { get; set; }
                public double Balance { get; set; }
      
                public int CompareTo(Customer other)
                {
                    // Sorting by CustId
                    if (this.CustId > other.CustId)return 1;
                    else if (this.CustId < other.CustId) return -1;
                    else return 0;
                }
            }
      
            class BalanceComparer : IComparer<Customer>
            {
                public int Compare(Customer x, Customer y) 
                {
                    // Sorting by Balance
                    if (x.Balance > y.Balance) return 1;
                    else if (x.Balance < y.Balance) return -1;
                    else return 0; 
                }
            }
      
            class DictionaryTest
            {
                static void Main()
                {
                    // Create instances of Customer
                    Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
                    Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
                    Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
                    Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
                    Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
                    Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };
      
                    // Assign all instances to a list
                    List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };
      
                    //Create instance
                    BalanceComparer balanceComparer = new BalanceComparer();
                    cus.Sort(1, 4, balanceComparer); //Index start from '0'
      
                    // Retrieve all values sorted by Balance
                    Console.WriteLine("Sorted by Balance:");
                    foreach (Customer obj in cus)
                    {
                        Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
                    }
                }
            }
        }
      

      Out:-

        Sorted by Balance:
        101 Mritunjay Hyderabad 25000
        103 Rahul Chennai 21000
        104 Amit Delhi 21500
        105 Mohan Jharkhand 21800
        102 Sumit Delhi 24000
        106 Gudu Uttar Pradesh 23800
      

      Remaining index numbers 0 and 5 are not included in sorting; the others are included in sorting by Balance.

Sort using Delegate2nd Sort -> Sort(Comparison<T> comparison):

Comparison is a delegate and this method have must same signature return type is integer and take two parameter.

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)return 1;
            else if (this.CustId < other.CustId) return -1;
            else return 0;
        }
    }

    class DictionaryTest
    {
        //Create method:
        public static int ComparteByName(Customer x, Customer y)
        {
            return x.Name.CompareTo(y.Name);
        }
        static void Main()
        {
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };


            //Pass 'ComparteByName' method in 'Comparison' delegant and also signature is matching both of them 'Comparison' delegant and 'ComparteByName' method
            Comparison<Customer> compDele = new Comparison<Customer>(ComparteByName);

            // Sort the list using the Comparison delegate
            cus.Sort(compDele);
            //Or
            //cus.Sort(CompareByName);

            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}
//Out:-
/*
104 Amit Delhi 21500
106 Gudu Uttar Pradesh 23800
105 Mohan Jharkhand 21800
101 Mritunjay Hyderabad 25000
103 Rahul Chennai 21000
102 Sumit Delhi 24000
*/

When using cus.Sort(CompareByName) and cus.Sort(compDele), both achieve the same result, but in slightly different ways:

  • cus.Sort(CompareByName): Directly passes the CompareByName method to the Sort() method. Since the CompareByName method matches the Comparison<Customer> delegate signature, the Sort() method internally creates a delegate for you.

  • cus.Sort(compDele): Explicitly uses a Comparison<Customer> delegate (compDele) that was previously created using the CompareByName method.

Using an anonymous method shortens the code:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)return 1;
            else if (this.CustId < other.CustId) return -1;
            else return 0;
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };

            // Sort the list using the Comparison delegate
            cus.Sort(delegate (Customer x, Customer y)
            {
                return x.Name.CompareTo(y.Name);
            });

            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}

Using a lambda expression shortens the code cus.Sort((s1, s2) => s1.Name.CompareTo(s2.Name));:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Customer : IComparable<Customer>
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }

        public int CompareTo(Customer other)
        {
            // Sorting by CustId
            if (this.CustId > other.CustId)return 1;
            else if (this.CustId < other.CustId) return -1;
            else return 0;
        }
    }

    class DictionaryTest
    {
        static void Main()
        {
            Customer c1 = new Customer { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 };
            Customer c2 = new Customer { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 };
            Customer c3 = new Customer { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 };
            Customer c4 = new Customer { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 };
            Customer c5 = new Customer { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 };
            Customer c6 = new Customer { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 };

            List<Customer> cus = new List<Customer>() { c1, c2, c3, c4, c5, c6 };


            // Sort the list using the Comparison delegate
            cus.Sort((s1, s2) => s1.Name.CompareTo(s2.Name));

            foreach (Customer obj in cus)
            {
                Console.WriteLine($"{obj.CustId} {obj.Name} {obj.City} {obj.Balance}");
            }
        }
    }
}

The CompareTo method for strings compares two strings to determine their order:

  1. Comparison:

    • Lexicographical Order: Strings are compared character by character based on their Unicode values (similar to alphabetical order).
  2. Return Values:

    • Negative: If the first string comes before the second string.

    • Zero: If both strings are equal.

    • Positive: If the first string comes after the second string.

Example:

string str1 = "apple";
string str2 = "banana";

int result = str1.CompareTo(str2); // result will be negative because "apple" comes before "banana"

Summary: CompareTo for strings checks each character's Unicode value to sort them in alphabetical order.

IEnumerable Interface:

The IEnumerable interface is the parent of all collection types.

  • IEnumerable Interface: This is the base interface for all non-generic collections. It defines a single method, GetEnumerator(), which returns an IEnumerator.

  • ICollection Interface: This inherits from IEnumerable and adds methods for size, enumerators, and synchronization.

  • IList and IDictionary Interfaces: These inherit from ICollection.

    • IList Interface: This represents collections of objects that can be individually accessed by index. Classes like ArrayList are part of this.

    • IDictionary Interface: This represents a collection of key/value pairs. Classes like Hashtable and Dictionary are part of this.

Here's a simplified visual hierarchy:

IEnumerable
 ├── ICollection
 │   ├── IList
 │   │   └── ArrayList
 │   └── IDictionary
 │       ├── Hashtable
 │       └── Dictionary

Inside the IEnumerable interface ICollection class is there, inside the ICollection class IList and IDictionary class ther and all list related class in inside the IList class like ArrayList and inside the IDictionary class all class which take key and value pair like Hashtable , Dictionary are available.

In the previous examples, you were able to extract all values from lists and dictionaries because every collection inherits from the IEnumerable interface. The IEnumerable interface internally has a method called GetEnumerator(). When a class implements IEnumerable, it must also implement the GetEnumerator() method, which is responsible for enabling the foreach loop to iterate through the collection.

If you look at the definition of the List class, you will see that it inherits from IEnumerable<T>. The IEnumerable<T> interface, in turn, inherits from the non-generic IEnumerable. The IEnumerable interface contains the GetEnumerator() method. Without the GetEnumerator() method, the foreach loop would not work. This is why you can use foreach with any collection that implements IEnumerable.

Note's:

  • IEnumerable is the base interface for all collections.

  • ICollection adds size, enumerators, and synchronization methods.

  • IList is for index-based collections.

  • IDictionary is for key/value pair collections.

  • The GetEnumerator() method in IEnumerable enables the foreach loop.

Note's:

When you use a foreach loop, the foreach loop internally calls the GetEnumerator method to get an enumerator. And enumerator is an object that enables iteration over a collection. enumerator provides methods like MoveNext and Reset, and a property called Current. Enumerators come from collections that implement the IEnumerable or IEnumerable<T> interface (Enumerators are used with collections that have IEnumerable or IEnumerable<T>.).

Example Code:

Here I create one class that works like a list:

Without IEnumerable Implementation:

using System;
using System.Collections.Generic;

namespace Collection
{
    public class Employee
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    //Non-Generic
    public class Orginization
    {
        //Hold the value we use Array:
        List<Employee> Emps = new List<Employee>();
        public void AddEmp(Employee emp)
        {
            Emps.Add(emp);
        }
    }

    class IEnumerableTest
    {
        static void Main()
        {
            Orginization orgEmp = new Orginization();
            orgEmp.AddEmp(new Employee { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 });
            orgEmp.AddEmp(new Employee { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 });
            orgEmp.AddEmp(new Employee { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 });
            orgEmp.AddEmp(new Employee { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 });
            orgEmp.AddEmp(new Employee { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 });
            orgEmp.AddEmp(new Employee { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 });

            foreach (Employee Emp in orgEmp)//Give error hear in 'orgEmp'
                Console.WriteLine(Emp.CustId + " " + Emp.Name + " " + Emp.City + " " + Emp.Balance);
        }
    }
}

The above code will give an error: foreach statement cannot operate on variables of type 'Orginization' because 'Orginization' does not contain a public instance or extension definition for 'GetEnumerator'.

Explain MethodAddEmp:

The method AddEmp in the Orginization class is designed to add an Employee object to the Emps list, which holds all the Employee objects.

public void AddEmp(Employee emp)
{
    Emps.Add(emp);
}
  • (Employee emp): The method takes one parameter, which is an Employee object named emp.

  • Emps.Add(emp);: This line adds the Employee object passed as a parameter (emp) to the Emps list. Emps is a list of Employee objects, declared as List<Employee> Emps = new List<Employee>(); in the Orginization class. The Add method of the List<T> class appends the specified element to the end of the list.

  • Purpose: The AddEmp method provides a way to add new Employee objects to the Orginization's internal list (Emps). By encapsulating the Add functionality within this method,

Explain ListList<Employee> Emps = new List<Employee>(); :

  • List<T>: This is a generic collection class in the System.Collections.Generic namespace. In this case, T is replaced with Employee, so List<Employee> is a list that holds Employee objects.

  • List<Employee> Emps :The variable Emps is declared to hold a reference to a List<Employee> object.

  • new List<Employee>() creates a new, empty list of Employee objects. At this point, Emps is an empty list, ready to have Employee objects added to it. new is a keyword which used to create a new instance of an object. List<Employee>() This is the constructor of the List<T> class not the constructor of the Employee class, which initializes a new instance of the list.

  • The Employee class constructor is not called in new List<Employee>() statement. The Employee class constructor will only be called when you create instances of the Employee class itself, such as when you add new Employee objects to the list. new List<Employee>() is the constructor of the List<T> class that is being called.

  • The Employee class would be a user-defined class. Each instance of the Employee class represents a single employee with properties such as CustId, Name, City, and Balance.

  • new List<Employee>() initializes a new list, and each new Employee { ... } initializes new instances of the Employee class. The Employee class constructor is called when creating new Employee objects, not when initializing the list.

  • When you add Employee instances to this list, the Employee class constructor is called for each new Employee object. For example: Emps.Add(new Employee { CustId = 101, Name = "John Doe", City = "New York", Balance = 1000.0 }); In this line new Employee { CustId = 101, Name = "John Doe", City = "New York", Balance = 1000.0 } calls the Employee class constructor to create a new instance of Employee and initialize it with the provided properties.

WithIEnumerableImplementation:

To fix this, you need to inherit IEnumerable and add the GetEnumerator method in the Orginization class:

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

namespace Collection
{
    public class Employee
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    //Non-Generic
    public class Orginization : IEnumerable
    {
        List<Employee> Emps = new List<Employee>();
        public void AddEmp(Employee emp) 
        {
            Emps.Add(emp);
        }
        public IEnumerator GetEnumerator()
        {
            return Emps.GetEnumerator();//Return type is GetEnumerator
        }
    }//This class work like collection now for employes
    class IEnumerableTest
    {
        static void Main()
        {
            Orginization orgEmp = new Orginization();
            orgEmp.AddEmp(new Employee { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 });
            orgEmp.AddEmp(new Employee { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 });
            orgEmp.AddEmp(new Employee { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 });
            orgEmp.AddEmp(new Employee { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 });
            orgEmp.AddEmp(new Employee { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 });
            orgEmp.AddEmp(new Employee { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 });

            foreach (Employee Emp in orgEmp)
                Console.WriteLine(Emp.CustId + " " + Emp.Name + " " + Emp.City + " " + Emp.Balance);
        }
    }
}
/*Out:-
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
*/

This implementation of Orginization class behave like a collection of Employee objects for employees by implementing the IEnumerable interface. This allows it to be used in a foreach loop.

IEnumerable Implementation: The GetEnumerator() method returns the enumerator of the Emps list, allowing the Orginization class to be used in a foreach loop.

  • The Orginization object orgEmp is created.

  • Several Employee objects are added to orgEmp using the AddEmp method.

  • The foreach loop iterates over orgEmp, printing the details of each Employee.

  • The GetEnumerator() method is used to enable iteration over the collection. It provides the necessary functionality for the foreach loop to iterate through the elements of the Orginization class.

  • Method Signaturepublic IEnumerator GetEnumerator(): This indicates a public method that returns an IEnumerator object. The IEnumerator interface provides the basic mechanisms for iterating over a collection.

    I told you earlier: When you use a foreach loop, the foreach loop internally calls the GetEnumerator method to get an enumerator. And enumerator is an object that enables iteration over a collection. enumerator provides methods like MoveNext and Reset, and a property called Current. Enumerators come from collections that implement the IEnumerable or IEnumerable<T> interface (Enumerators are used with collections that have IEnumerable or IEnumerable<T>.).

  • return Emps.GetEnumerator(); : Emps is a List<Employee>. The List<T> class implements IEnumerable<T>, so it has a GetEnumerator method that returns an enumerator. We also used Emps.GetEnumerator();. The GetEnumerator method of the List<T> class is called, which returns an enumerator object that can iterate through the list of employees.

  • In foreach loop calls orgEmp.GetEnumerator() to get an enumerator. The enumerator is then used to iterate over each Employee object in the Emps list.

Custom Enumerator Implementation:

If you do not want to return the List class GetEnumerator, you can create your own enumerator:

public IEnumerator GetEnumerator()
{
    throw new NotImplementedException();
}

return the GetEnumerator:

public IEnumerator GetEnumerator()
{
    return Emps.GetEnumerator();//Return type is GetEnumerator
}

If you do not want to return the List class GetEnumerator, you can create your own enumerator:

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

namespace Collection
{
    public class Employee
    {
        public int CustId { get; set; }
        public string Name { get; set; }
        public string City { get; set; }
        public double Balance { get; set; }
    }

    public class Orginization : IEnumerable
    {
        List<Employee> Emps = new List<Employee>();

        public void AddEmp(Employee emp)
        {
            Emps.Add(emp);
        }

        public IEnumerator GetEnumerator()
        {
            return new OrginizationEnumerator(this);
        }

        public int Count => Emps.Count;

        public Employee this[int index] => Emps[index];
    }

    public class OrginizationEnumerator : IEnumerator
    {
        Orginization orgColl;
        int currentIndex;
        Employee currentEmployee;

        public OrginizationEnumerator(Orginization org)
        {
            orgColl = org;
            currentIndex = -1;
        }

        public object Current => currentEmployee;

        public bool MoveNext()
        {
            if (++currentIndex >= orgColl.Count)
                return false;
            else
            {
                currentEmployee = orgColl[currentIndex];  //Set the index
                return true;
            }
        }

        public void Reset()
        {
            currentIndex = -1;
        }
    }

    class IEnumerableTest
    {
        static void Main()
        {
            Orginization orgEmp = new Orginization();
            orgEmp.AddEmp(new Employee { CustId = 101, Name = "Mritunjay", City = "Hyderabad", Balance = 25000.00 });
            orgEmp.AddEmp(new Employee { CustId = 102, Name = "Sumit", City = "Delhi", Balance = 24000.00 });
            orgEmp.AddEmp(new Employee { CustId = 103, Name = "Rahul", City = "Chennai", Balance = 21000.00 });
            orgEmp.AddEmp(new Employee { CustId = 104, Name = "Amit", City = "Delhi", Balance = 21500.00 });
            orgEmp.AddEmp(new Employee { CustId = 105, Name = "Mohan", City = "Jharkhand", Balance = 21800.00 });
            orgEmp.AddEmp(new Employee { CustId = 106, Name = "Gudu", City = "Uttar Pradesh", Balance = 23800.00 });

            foreach (Employee Emp in orgEmp)
                Console.WriteLine($"{Emp.CustId} {Emp.Name} {Emp.City} {Emp.Balance}");
        }
    }
}
/*Out:-
101 Mritunjay Hyderabad 25000
102 Sumit Delhi 24000
103 Rahul Chennai 21000
104 Amit Delhi 21500
105 Mohan Jharkhand 21800
106 Gudu Uttar Pradesh 23800
*/

Purpose ofOrginizationEnumeratorclass: The OrginizationEnumerator class is an implementation of the IEnumerator interface. It is designed to iterate over a collection of Employee objects contained in an Orginization class.

Fields:

  • orgColl: Holds a reference to the Orginization instance being enumerated.

  • currentIndex: Keeps track of the current position in the collection. Initialized to -1 to start before the first element.

  • currentEmployee: Holds the current Employee object during iteration.

Constructor:

public OrginizationEnumerator(Orginization org)
{
    orgColl = org;
    currentIndex = -1;
}
  • Initializes orgColl with the given Orginization object.

  • Index start from 0. Sets currentIndex to -1, meaning the enumerator starts before the first element.

Method: MoveNext():

public bool MoveNext()
{
    if (++currentIndex >= orgColl.Count)
        return false;
    else
    {
        currentEmployee = orgColl[currentIndex];
        return true;
    }
}
  • Moves the enumerator to the next element.

  • Increases currentIndex.

  • if (++currentIndex >= orgColl.Count) return false; :If currentIndex is equal to or more than the total number of elements, it returns false, meaning the end of the collection.

  • currentEmployee = orgColl[currentIndex];: it updates currentEmployee with the new element and returns true.

Property: Current: public object Current => currentEmployee; Returns the current Employee object. It is typed as object to match the IEnumerator interface. You can also write like that:

public object Current //Current is use to access the current record. 
{
    get{ return CurrectEmployee; }
}

Method: Reset():

public void Reset()
{
    currentIndex = -1;
}

Resets the enumerator to its initial state, before the first element. This is often used to restart enumeration. Actually, it's not required at that time; you can simply write it like this: public void Reset(){}.

Why We NeedMoveNext(),Reset(), andCurrent:

MoveNext(), Reset(), and Current are part of the IEnumerator interface, which is used for iterating over a collection.

  1. MoveNext() Method:
  • Purpose: Moves the enumerator to the next element in the collection.

  • What It Does: It increments the internal index and checks if it is still within the bounds of the collection. If it is, it updates the Current property to the new element and returns true. If it’s out of bounds (i.e., past the last element), it returns false and the iteration ends.

  • Why We Need It: This method is essential for advancing through the collection. The foreach loop calls MoveNext() to progress to the next item.

  1. Reset() Method:
  • Purpose: Resets the enumerator to its initial position, before the first element.

  • What It Does: It sets the index back to -1 or the starting position. This allows for re-iteration or starting the iteration again from the beginning.

  • Why We Need It: While Reset() is less commonly used in typical iteration scenarios, it can be useful if you need to iterate over the collection again using the same enumerator.

  1. Current Property:
  • Purpose: Gets the current element in the collection.

  • What It Does: It provides access to the element at the current position of the enumerator.

  • Why We Need It: This property returns the element that the enumerator is currently pointing to. During iteration, it allows you to access the item that MoveNext() has moved to.

Purpose ofGetEnumeratorMethod: Allows the Orginization class to be used with foreach loops.

public IEnumerator GetEnumerator()
{
    return new OrginizationEnumerator(this);
}

Returns a new OrginizationEnumerator initialized with the current instance of Orginization. It means, when GetEnumerator() is called, it constructs a new OrginizationEnumerator object. And this enumerator is initialized with the current Orginization instance, allowing it to access and iterate through the Employee objects in that specific Orginization instance.

The foreach loop calls GetEnumerator() to get a new OrginizationEnumerator object each time. Then uses this object to go through the items in the Orginization collection.

  • newKeyword: The new keyword is used to create a new instance of a class or struct. This means it allocates memory for the new object and calls its constructor to set it up. In this case, new is used to create a new OrginizationEnumerator object. This new object will be in charge of going through the items in the Orginization collection.

  • thisKeyword: The this keyword refers to the current instance of the class where it is used. It provides a way to access the current object’s members (like methods and properties). It's often used to pass the current instance to other methods or constructors. Here, this is used to pass the current instance of the Orginization class to the constructor of OrginizationEnumerator. This allows the enumerator to access the collection of employees in the Orginization class.

Indexing and Counting:

Indexing and Counting are features of the Orginization class that enhance its functionality:

CountProperty:public int Count => Emps.Count;

  • Purpose: Provides the number of Employee items in the Orginization.

  • What It Does: Returns the count of items in the Emps list.

  • Why We Need It: This property is useful for determining the number of elements in the collection, especially if you need to perform operations based on the size of the collection.

  •                             public int Count => Emps.Count; //Use to Counting
                                //Or
                                public int Count
                                {
                                    get { return Emps.Count; }
                                }
    

Indexer (this[int index]): public Employee this[int index] => Emps[index];

  • Purpose: Allows access to Employee objects in the Orginization collection using an index, similar to array indexing.

  • What It Does: Retrieves the Employee at the specified index from the Emps list.

  • Why We Need It: This indexer provides a way to access elements directly by index, making the Orginization class behave like an array or list. This can be useful for direct access to items without needing to iterate through the collection.

      public Employee this[int index] => Emps[index]; //Use to Indexing
      //Or
      public Employee this[int index] 
      {
          get{ return Emps[index]; }
      }
    
    • this: The keyword that indicates this is an indexer, not a regular property. It allows the class to be indexed.

    • [int index]: The parameter list for the indexer. It specifies that the indexer will take an integer parameter, which represents the index of the element to access.

    • Employee: The return type of the indexer. It specifies that the indexer will return an Employee object.

    • Emps[index]: The body of the getter. It returns the Employee object at the specified index from the internal Emps list.

    • UseEmployee emp = orgEmp[0];:

      here's what happens:

      • orgEmp[0]: The indexer is called with the index 0.

      • get { return Emps[0]; }: The getter of the indexer is executed, which returns Emps[0].

      • Emps[0]: This accesses the Emps list (which is a List<Employee>) and retrieves the Employee object at index 0.

When a foreach loop is used with an Organization object, it looks for the GetEnumerator method in the Organization class. The Organization class implements the IEnumerable interface, which requires a GetEnumerator method that returns an IEnumerator. The IEnumerator implementation is provided by the OrganizationEnumerator class, where we define the logic for the Current property and the MoveNext method.

In that example, we have 4 classes:

  1. Employee class

  2. Organization class: This class works like a collection. We define the AddEmp method to add employees, the Count property to return the number of items, the indexer to return an item by index, and the GetEnumerator method to enable the foreach loop. This GetEnumerator method returns an IEnumerator type, which is an interface. Next, we define an OrganizationEnumerator class to implement the IEnumerator interface.

  3. OrganizationEnumerator class: This class inherits from IEnumerator. I define a constructor to access the Organization, and then I implement the Current property and two methods, MoveNext and Reset, although we do not use Reset.

  4. IEnumerableTest class: This is the main class. Here, we use the main method to create an instance of the Organization class called orgEmp. Then, we add employees and use a foreach loop to get the data.

Q. What isIEnumerable,IEnumeratorandGetEnumerator?

Ans:-

IEnumerable: IEnumerable is an interface used by all collection classes. It includes a method called GetEnumerator. Because of GetEnumerator, we can use a foreach loop on the collection. If we want our classes to act like collections, we need to implement the GetEnumerator method.

IEnumerator: An interface that defines methods for iterating over a collection. MoveNext(): Moves to the next item. Reset(): Resets to the position before the first item. Current: Gets the current item in the collection.

GetEnumerator: A method from IEnumerable that returns an IEnumerator. It allows us to use a foreach loop to iteration the collection.

Q. CRUD operations using Collection (Generic Collection)?

  • Create a Product class

  • It has the following properties like Id, name, price

  • Create a collection of Product class

  • Add three products and display

  • We need to do the following operations

    • Add a New Product

    • Update an existing product price

    • Display product by id

    • Display all products

    • Delete a product by taking id

Ans:

Edit this text


LINQ(Language Integrated Query):

LINQ is a query language designed by Microsoft in .NET 3.5. LINQ allows you to write queries on various data sources such as arrays, collections, database tables, datasets, and XML data.

LINQ is available in the System.Linq namespace.

Consider the task of sorting an array. Traditionally, you might use loops and predefined methods to do this. LINQ offers a simpler and more elegant way to handle such tasks.

Here's an example array:

int[] numbers = { 17, 34, 8, 56, 23, 91, 42, 73, 15, 27, 68, 39, 44, 53, 11, 22, 78, 31, 86, 9 };

To sort this array using LINQ, you can use the following syntax. LINQ syntax is similar to SQL, where you select and manipulate data.

SQL Syntax:

SELECT <column_list> FROM <table> [AS <alias>] [<clauses>]

LINQ Syntax:

from <alias> in <collection | array> [<clauses>] select <alias>

Getting All Data from an Array Using LINQ:

var num1 = from i in numbers select i;

var is a keyword introduced in C# 3.0. It declares an implicitly typed local variable, And the data type of var is determined by the value it holds.

num1 is used to capture the array data from from i in numbers select i. num1 is now an array.

Sort the data which is greater than 40:

var numbers1 = from i in numbers where i > 40 select i;

Sort the data which is greater than 40 in ascending order:

var numbers1 = from i in numbers where i > 40 orderby i select i;

Sort the data which is greater than 40 in descending order:

var numbers1 = from i in numbers where i > 40 orderby i descending select i;

Display values:

foreach (var item in numbers1)
    Console.Write(item + " ");

LINQ to SQL:

  • It's a query language that was introduced in the .NET 3.5 framework for working with relational databases, such as SQL Server.

  • LINQ to SQL is not just for querying data, it also lets us perform CRUD operations.

  • CRUD: Create(Insert), Read(Select), Update, Delete**.**

We can also call stored procedures using LINQ to SQL.

Q. There is already a language known as SQL, which we can use to interact with SQL Server with the help of ADO.Net. Then why do we need LINQ?

Ans: SQL is a powerful language used to interact with SQL Server via ADO.NET. However, LINQ (Language Integrated Query) has several benefits over traditional SQL when used in a .NET environment. Let's look at why LINQ is needed and how it can be better than SQL in some cases.

Advantages of LINQ over SQL in ADO.NET:

  • Compile-Time Syntax Checking:

    • SQL in ADO.NET: When you write an SQL query in ADO.NET, it's usually wrapped in double quotes as a string. The .NET compiler doesn't recognize the syntax of this string. The query is sent to the SQL Server, where the database engine validates the syntax. If there's a syntax error, it's caught at runtime, which can increase the load on the database engine.

    • LINQ: LINQ queries are checked for syntax errors at compile time by the .NET compiler. This means errors are caught earlier, reducing runtime issues and lowering the load on the database engine.

  • Type Safety:

    • SQL in ADO.NET: SQL queries in ADO.NET are not type-safe. For example, if your query tries to insert a value into a table with a mismatched data type or too many columns, the database engine will return an error. This error is only caught at runtime, which can lead to inefficiencies and wasted time.

    • LINQ: LINQ is completely type-safe. The .NET compiler makes sure that the types of values in your queries match the database schema. This type checking happens on the client side, making development safer and more efficient. Visual Studio's IntelliSense helps you see the data types of columns, avoiding type mismatches.

  • IntelliSense Support:

    • SQL in ADO.NET: When writing SQL queries in ADO.NET, you don't get IntelliSense support for column names, table names, or data types, which can lead to errors and slower development.

    • LINQ: LINQ offers full IntelliSense support in Visual Studio. This means you get real-time suggestions and feedback on the structure of your query, making development faster and more error-free.

  • Debugging Capabilities:

    • SQL in ADO.NET: Debugging SQL statements is hard because the SQL code runs on the database server. You can't step through SQL code like you can with .NET code.

    • LINQ: LINQ queries are written in C# or VB.NET and run on the client side. This allows you to debug LINQ queries just like any other .NET code, making the debugging process easier.

  • Pure Object-Oriented Code:

    • SQL in ADO.NET: SQL code in ADO.NET often feels separate from the rest of your application. For example, when inserting data into a table, you have to manually join strings, which can lead to SQL injection risks and makes the code harder to maintain.

    • LINQ: LINQ lets you work with data in a fully object-oriented way. Tables become classes, columns become properties, rows are instances of those classes, and stored procedures are methods. This makes the code more consistent, easier to maintain, and in line with modern programming practices.

  • Simplified Code Structure:

    • SQL in ADO.NET: SQL queries can make the codebase a mix of object-oriented and relational code, which can be harder to maintain.

    • LINQ: LINQ allows developers to write queries that follow object-oriented principles. This results in cleaner, more readable, and easier-to-maintain code.

Working with LINQ to SQL:

To work with LINQ to SQL, we first need to convert all the relational objects of the database into object-oriented types. This process is known as ORM (Object-Relational Mapping).

Working with LINQ to SQL: To work with LINQ to SQL, first we need to convert all the relational objects of the database into object oriented types. This process is known as ORM (Object Relational Mapping).

To perform ORM, we use a tool called the OR designer.

Steps to Perform ORM Using LINQ to SQL:

  1. Using the Object-Relational (OR) Designer:

    • ORM with OR Designer: The OR Designer is a tool provided by Visual Studio that helps you perform ORM by visually mapping database tables, views, and stored procedures to corresponding classes, properties, and methods in your .NET application.
  2. Adding a Reference to the System.Data.Linq Assembly:

    • To work with LINQ to SQL, you need to add a reference to the System.Data.Linq.dll assembly in your project. This assembly contains the necessary classes and methods for working with LINQ to SQL.
  3. Configuring the Connection String:

    • You need to write the connection string in the configuration file (typically App.config or Web.config) of your project. This connection string will provide the necessary information for your application to connect to the SQL Server database.

To add the OR Designer, select New Item and choose LINQ to SQL Classes. The extension is .dbml (Database Markup Language). Name the file, ideally matching the database name. Then go to Solution Explorer > References and check System.Data.Linq. If you cannot find the "LINQ to SQL Classes" option, it might be because it is a legacy feature not included by default in newer versions of Visual Studio. To enable it:

  1. Open Visual Studio Installer.

  2. Ensure that ASP.NET and web development is checked under the Workloads tab.

  3. Go to the Individual components tab, search for LINQ to SQL tools, and check it.

  4. Click Modify to install the required components.

  5. After installation, return to Visual Studio, select New Item again, and search for LINQ to SQL Classes.

When you create a .dbml file, it generates two additional files:

  1. <filename>.dbml.layout

  2. <filename>.designer.cs

The designer.cs file is automatically generated by Visual Studio and is not meant to be edited manually. Visual Studio writes the code in this file when you drag and drop database objects onto the OR Designer. You should only view this file, not modify it.

Inside the designer.cs file, a class is generated with the name <filename>DataContext. This class is a partial class that inherits from System.Data.Linq.DataContext.

What does the <filename>DataContext class do?

This class acts as a connection to the database. In ADO.NET, you would use a SqlConnection class to connect to a database, but in LINQ to SQL, the <filename>DataContext class serves this purpose. When you create an instance of this class, it helps establish the connection to the database.

This class needs a connection string, which specifies the database URL. This connection string is usually stored in the App.config file. When you create an instance of the <filename>DataContext class, it reads the connection string from App.config and establishes the connection to the database.

The two panels in the OR Designer in Visual Studio serve specific purposes:

  1. Diagram Panel: This left panel lets you visually design your data model by dragging and dropping tables, views, and relationships. It provides a graphical overview of your database schema.

  2. Properties Panel: This right panel shows detailed information about the selected item in the diagram, such as column names, data types, and relationships. It allows for precise adjustments and configurations.

In my project "LinqToSqlProject," the database name is also "LinqToSqlProject," and I named my .dbml file as DataClasses1.dbml.

Steps:

  1. Drag and Drop Tables:

    • Go to the "Tables" section (under the "Tables" folder) in Server Explorer.

    • Select the table (e.g., Employee) and drag it to the Diagram Panel in the OR Designer.

  2. Connection String in App.config:

    • After performing the above step, Visual Studio automatically adds the connection string to the App.config file. It might look like this:

    • App.config:

        <?xml version="1.0" encoding="utf-8" ?>
        <configuration>
            <configSections>
            </configSections>
            <connectionStrings>
                <add name="LinqToSqlProject.Properties.Settings.LinqToSqlProjectConnectionString"
                    connectionString="Data Source=DESKTOP-HOOMVQE\MSSQLSERVER02;Initial Catalog=LinqToSqlProject;Integrated Security=True;Encrypt=True;TrustServerCertificate=True"
                    providerName="System.Data.SqlClient" />
            </connectionStrings>
            <startup> 
                <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
            </startup>
        </configuration>
      
  3. Generated Code in designer.cs:

    • The DataClasses1.dbml file generates a designer.cs file. This file is not meant for manual editing; Visual Studio manages it automatically when you drag and drop items in the designer.

    • The designer.cs file includes a parameterless constructor in the DataClasses1DataContext class, which reads the connection string:

        public DataClasses1DataContext() : 
            base(global::LinqToSqlProject.Properties.Settings.Default.LinqToSqlProjectConnectionString, mappingSource)
        {
            OnCreated();
        }
      
    • Additionally, a property is created for each table, matching the table name. For example, if your table is named Employee, the code will look like this:

        public System.Data.Linq.Table<Employee> Employees
        {
            get
            {
                return this.GetTable<Employee>();
            }
        }
      
    • A class named Employee is also generated, with fields and properties corresponding to the columns of the Employee table. The fields use an underscore prefix (_):

        public partial class Employee
        {
      
            private System.Nullable<int> _Eno;
      
            private string _Ename;
      
            private string _Job;
      
            private System.Nullable<decimal> _Salary;
      
            private string _Dname;
      
            public Employee()
            {
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Eno", DbType="Int")]
            public System.Nullable<int> Eno
            {
                get
                {
                    return this._Eno;
                }
                set
                {
                    if ((this._Eno != value))
                    {
                        this._Eno = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Ename", DbType="VarChar(50)")]
            public string Ename
            {
                get
                {
                    return this._Ename;
                }
                set
                {
                    if ((this._Ename != value))
                    {
                        this._Ename = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Job", DbType="VarChar(50)")]
            public string Job
            {
                get
                {
                    return this._Job;
                }
                set
                {
                    if ((this._Job != value))
                    {
                        this._Job = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Salary", DbType="Money")]
            public System.Nullable<decimal> Salary
            {
                get
                {
                    return this._Salary;
                }
                set
                {
                    if ((this._Salary != value))
                    {
                        this._Salary = value;
                    }
                }
            }
      
            [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Dname", DbType="VarChar(50)")]
            public string Dname
            {
                get
                {
                    return this._Dname;
                }
                set
                {
                    if ((this._Dname != value))
                    {
                        this._Dname = value;
                    }
                }
            }
        }
      

Note: The rows or records in the database are represented as instances of the Employee class when the program is running.

  1. Load the data:

    • To load the data, we use the Employees property, which returns the Table.

    • Example get data from database :

      Drag and drop the DataGridView in Form1.cs.

      Page: Form1.cs:

        using System;
        using System.Windows.Forms;
      
        namespace LinqToSqlProject
        {
            public partial class Form1 : Form
            {
                public Form1()
                {
                    InitializeComponent();
                }
      
                private void Form1_Load(object sender, EventArgs e)
                {
                    //Establish the connection
                    DataClasses1DataContext data = new DataClasses1DataContext();
                    //get the property
                    dataGridView1.DataSource = data.Employees;
                }
      
                private void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
                {
      
                }
            }
        }
      

      Server Explorer:

      Toolbox:

      DataClasses1.dbml:

      SQL Server:

      Get the data in Form1: when run the Form1

That's how ORM works.


Q. Create the Form to see the employee data one by one?

Ans:- Steps:

  1. Open SSMS:

    Create SQL Server

    Notes: Make sure to check the Trust server certificate option, otherwise you will get an error.

  2. Create databas and add data:

     Create database Company;
    
     CREATE TABLE Employee (
         EmployeeID INT PRIMARY KEY,
         FirstName NVARCHAR(50),
         LastName NVARCHAR(50),
         JobTitle NVARCHAR(100),
         Salary DECIMAL(10, 2),
         Department NVARCHAR(50),
         HireDate DATE
     );
    
     INSERT INTO Employee (EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate) VALUES
     (1, 'John', 'Doe', 'Software Engineer', 75000.00, 'IT', '2022-01-15'),
     (2, 'Jane', 'Smith', 'Data Analyst', 68000.00, 'IT', '2021-03-22'),
     (3, 'Michael', 'Johnson', 'Project Manager', 85000.00, 'Operations', '2020-08-30'),
     (4, 'Emily', 'Davis', 'HR Specialist', 60000.00, 'Human Resources', '2019-11-05'),
     (5, 'David', 'Brown', 'Senior Developer', 90000.00, 'IT', '2018-07-17'),
     (6, 'Linda', 'Wilson', 'Marketing Manager', 78000.00, 'Marketing', '2023-04-12'),
     (7, 'Robert', 'Taylor', 'Accountant', 67000.00, 'Finance', '2021-12-01'),
     (8, 'Mary', 'Lee', 'Customer Support', 52000.00, 'Support', '2020-06-15'),
     (9, 'James', 'Martin', 'DevOps Engineer', 83000.00, 'IT', '2017-05-23'),
     (10, 'Patricia', 'Anderson', 'Business Analyst', 72000.00, 'Operations', '2019-02-14');
    

    Notes: Do not close the server

  3. Create Windows Form App:

    • Give the project name ShowEmployeeFromDatabase.

  4. Open Server Explorer and configure the database:

    • Copy the server name from SSMS, then go to Server Explorer and right-click Data Connections. Select the data source and press next. Enter the server name, then select the database from Select or enter a database name:. Press ok.

      Notes: Make sure to check the Trust server certificate option, otherwise you will get an error.

      If you do not see the database, please restart the system.

  1. ORM setup:

    • Add a new item LINQ to SQL (.dbml file).

    • Go to Server Explorer, open the Tables folder, and drag and drop the Employee table to the left side of Company.dbml. ORM setup is complete.

  2. Crete design:

    • Go to Solution Explorer and open Form1.cs, which is automatically created, or you can choose your own. Next, open Toolbox and then Common Control to create a design like this.

  3. Write the code in Form1.cs:

     using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Windows.Forms;
    
     namespace ShowEmployeeFromDatabase
     {
         public partial class Form1 : Form
         {
             CompanyDataContext dc; // DataContext instance to connect to the database
             List<Employee> employees; // List to hold Employee data
             int currentRecordIndex = 0; // Variable to track the current record index
    
             public Form1()
             {
                 InitializeComponent();
             }
    
             private void Form1_Load(object sender, EventArgs e)
             {
                 //CompanyDataContext dc = new CompanyDataContext();//Create instance
                 //List<Employee> emp = new List<Employee>();//emp is a list which store only Employee type of data
    
                 // Initialize the DataContext and fetch employee data into the list
                 dc = new CompanyDataContext();
                 employees = dc.Employees.ToList();
    
                 // Show the first record on form load
                 ShowData();
             }
    
             private void ShowData()
             {
                 // Display employee data in the respective text boxes
                 textBox2.Text = employees[currentRecordIndex].EmployeeID.ToString();
                 textBox1.Text = $"{employees[currentRecordIndex].FirstName} {employees[currentRecordIndex].LastName}";
                 textBox3.Text = employees[currentRecordIndex].JobTitle;
                 textBox4.Text = employees[currentRecordIndex].Salary.ToString();
                 textBox5.Text = employees[currentRecordIndex].Department;
                 textBox6.Text = employees[currentRecordIndex].HireDate.ToString("yyyy-MM-dd"); // Format the date for clarity
             }
    
             private void Prev_Click(object sender, EventArgs e)
             {
                 if (currentRecordIndex > 0) 
                 {
                     currentRecordIndex -= 1;
                     ShowData();
                 }
                 else
                 {
                     MessageBox.Show("This is the first record of the table!");
                 }
             }
    
             private void Next_Click(object sender, EventArgs e)
             {
                 if (currentRecordIndex < employees.Count - 1)
                 {
                     currentRecordIndex += 1;
                     ShowData();
                 }
                 else
                 {
                     MessageBox.Show("This is the last record of the table!");
                 }
             }
    
             private void Close_Click(object sender, EventArgs e)
             {
                 this.Close(); // Close the form when the close button is clicked
             }
         }
     }
    

    This code is a Windows Forms application in C# that connects to a database using LINQ to SQL. It displays employee records and allows navigation through them using "Next" and "Previous" buttons.

    Key Points:

    • DataContext (CompanyDataContext): Connects to the database.

    • Employee List (employees): Stores employee records fetched from the database.

    • Record Navigation:

      • currentRecordIndex tracks the current record.

      • ShowData() displays the current record's details in text boxes.

      • "Previous" and "Next" Buttons: Navigate through records.

    • Close Button: Closes the form.

  4. Output:

    If you don't see the output video here, go hear Outpur.

    The form loads employee data on startup and provides a simple interface for viewing and navigating records.


Performing CRUD Operations using LINQ:

  • CRUD: Create, Read, Update, and Delete.

Setup project:

  • Create a Windows application named CRUD_WindowsForm.

  • Configure the database in Server Explorer.

  • Use an existing database and table.

  • Create a DatabaseData.dbml file and add the employee table.

  • Create a Form1.cs file and design it to display the data:

    In the properties, name the Insert button InsertData, the Update button button2, the Delete button button4, and the Close button button3. Name the DataGridView as dataGridView1.

  • Create a Form2Insert.cs file and design it to insert data:

    In this form, name the 7 textBox controls from textBox1 to textBox7. Name the Submit button button1, the Clear button button2, and the Close button button3.

  • Create a Form3Update.cs file and design it to update data:

    In this form, name the 7 textBox controls from textBox1 to textBox7. Name the Submit button button1, the Clear button button2, and the Close button button3. Set the Modifiers property of all textBox controls to internal.

Codeing Part:

Form1.cs:

using System;
using System.Linq;
using System.Windows.Forms;

namespace CRUD_WindowsForm
{
    public partial class Form1 : Form
    {
        DatabaseDataDataContext dataContext;
        public Form1()
        {
            InitializeComponent();
        }

        //Data loader method
        private void LoadData()
        {
            dataContext = new DatabaseDataDataContext();
            dataGridView1.DataSource = dataContext.Employees;
        }

        //Form Load 1 time
        private void Form1_Load(object sender, EventArgs e)
        {
            LoadData();
        }

        //InsertData:-
        private void button1_Click(object sender, EventArgs e)
        {
            Form2Insert fi = new Form2Insert(); //Use form 2 to insert data
            fi.ShowDialog();
            LoadData();
        }

        //UpdateData:-
        private void button2_Click(object sender, EventArgs e)
        {
            if (dataGridView1.SelectedRows.Count > 0) // Check if a row is selected
            {
                DataGridViewRow selectedRow = dataGridView1.SelectedRows[0];

                Form3Update fu = new Form3Update(); // Initialize Form3Update //Use same form "form 2" to update the value by changing the modifier in textBox field private to internal
                fu.textBox1.ReadOnly = true;
                fu.button2.Enabled = false;
                fu.button1.Text = "Update";
                fu.textBox1.Text = selectedRow.Cells[0].Value.ToString() ?? String.Empty; ;
                fu.textBox2.Text = selectedRow.Cells[1].Value.ToString() ?? String.Empty; 
                fu.textBox3.Text = selectedRow.Cells[2].Value.ToString() ?? String.Empty; 
                fu.textBox4.Text = selectedRow.Cells[3].Value.ToString() ?? String.Empty; 
                fu.textBox5.Text = selectedRow.Cells[4].Value.ToString() ?? String.Empty; 
                fu.textBox6.Text = selectedRow.Cells[5].Value.ToString() ?? String.Empty; 
                fu.textBox7.Text = selectedRow.Cells[6].Value.ToString() ?? String.Empty;
                //'dataGridView1' is DataGridView name which is taken from property
                fu.ShowDialog();
                LoadData();
            }
            else
            {
                MessageBox.Show("Please select a row first.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); // Prompt user to select a row if none is selected
            }
        }

        //Close
        private void button3_Click(object sender, EventArgs e)
        {
            this.Close();
        }


        //Delete
        private void button4_Click(object sender, EventArgs e)
        {
            if (dataGridView1.SelectedRows.Count > 0) 
            {
                if (MessageBox.Show("Are you suore want to delete this data?", "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
                {
                    int Eno = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells[0].Value);
                    Employee obj = dataContext.Employees.SingleOrDefault(E => E.EmployeeID == Eno);
                    dataContext.Employees.DeleteOnSubmit(obj);
                    dataContext.SubmitChanges();
                    LoadData();
                }
            }
            else
            {
                MessageBox.Show("Please select a row first for deletion.", "Information",MessageBoxButtons.OK, MessageBoxIcon.Information); 
            }
        }
    }
}

Form2Insert.cs:

using System;
using System.Windows.Forms;

namespace CRUD_WindowsForm
{
    public partial class Form2Insert : Form
    {
        public Form2Insert()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            DatabaseDataDataContext db = new DatabaseDataDataContext();

            Employee eobj = new Employee();

            eobj.EmployeeID = int.Parse(textBox1.Text);
            eobj.FirstName = textBox2.Text;
            eobj.LastName = textBox3.Text;
            eobj.JobTitle = textBox4.Text;
            eobj.Salary = decimal.Parse(textBox5.Text);
            eobj.Department = textBox6.Text;
            eobj.HireDate = DateTime.Now;

            // Assuming you want to insert the new employee into the database
            db.Employees.InsertOnSubmit(eobj);
            db.SubmitChanges();
            MessageBox.Show("Employee added successfully!");
        }

        private void button2_Click(object sender, EventArgs e)
        {
            foreach (Control ctrl in this.Controls)
            {
                if(ctrl is TextBox)
                {
                    TextBox tb = ctrl as TextBox;
                    tb.Clear();
                }
            }
            textBox1.Focus();
        }

        private void button3_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}

Form3Update.cs

using System;
using System.Linq;
using System.Windows.Forms;

namespace CRUD_WindowsForm
{
    public partial class Form3Update : Form
    {
        public Form3Update()
        {
            InitializeComponent();
        }

        //Submit
        private void button1_Click(object sender, EventArgs e)
        {
            DatabaseDataDataContext db = new DatabaseDataDataContext();

            //We do not want to create new record we want to update that why we need the refrence of current record that why we use ' db.Employees.SingleOrDefault(E=>E.EmployeeID == int.Parse(textBox1.Text))'
            Employee eobj = db.Employees.SingleOrDefault(E => E.EmployeeID == int.Parse(textBox1.Text));

            if(eobj != null)
            {
                //If use not modify old value taken
                eobj.FirstName = textBox2.Text;
                eobj.LastName = textBox3.Text;
                eobj.JobTitle = textBox4.Text;
                eobj.Salary = decimal.Parse(textBox5.Text);
                eobj.Department = textBox6.Text;
                eobj.HireDate = DateTime.Now;

                db.SubmitChanges();
                MessageBox.Show("Employee update successfully!");
            }
            else
            {
                MessageBox.Show("Employee not found.");
            }
        }

        //Close
        private void button3_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}

Coding Explanation:

  • LoadData Method:

    The LoadData method is responsible for loading and displaying the employee data on Form1. Here is the method:

      private void LoadData()
      {
          dataContext = new DatabaseDataDataContext(); // Establish a connection to the database
          dataGridView1.DataSource = dataContext.Employees; // Bind the Employees table to the DataGridView
      }
    

    Explanation of the LoadData Method

    1. Connecting to the Database:

      • dataContext = new DatabaseDataDataContext();

      • This line initializes a new instance of DatabaseDataDataContext, which represents the database connection and provides access to the Employees table.

    2. Binding Data to the DataGridView:

      • dataGridView1.DataSource = dataContext.Employees;

      • This line sets the DataSource property of dataGridView1 (the grid displaying data) to the Employees table. This binds the employee data to the grid, displaying it on the form.

    3. dataContext is a variable of type DatabaseDataDataContext, which is a class. It is declared globally in the Form1 class as DatabaseDataDataContext dataContext;. And dataContext inslized by LoadData() method.

  • Close Button Functionality:

    For every Close button on your forms, the code is consistent. Double-click the Close button in the form designer, which will automatically generate an event handler. Inside this event handler, add the following line of code:

      this.Close();
    

    This will close the current form window, when the Close button is clicked.

  • Clear Button Functionality:

    Clears all text boxes in the form and sets the focus back to textBox1.

      private void button2_Click(object sender, EventArgs e)
      {
          foreach (Control ctrl in this.Controls)
          {
              if (ctrl is TextBox)
              {
                  TextBox tb = ctrl as TextBox;
                  tb.Clear();
              }
          }
          textBox1.Focus();
      }
    

    Explanation:

    • Control: In Windows Forms, a Control is a base class for all components that are displayed on a form (e.g., buttons, text boxes, labels, etc.).

    • this.Controls: Represents a collection of all the controls present on the form.

    • if (ctrl is TextBox): This line checks if the current control (ctrl) is a TextBox. is TextBox, the is keyword checks if an object is of a specific type. Here, it checks whether ctrl is a TextBox.

    • TextBox tb = ctrl as TextBox;: This line tries to convert the ctrl object into a TextBox. The as keyword is used for safe casting. If ctrl is a TextBox, it will be converted to a TextBox and assigned to the variable tb. If not, tb will be null.

    • tb.Clear();: This line clears the text inside the TextBox. Clear(): The Clear() method is a TextBox method that removes all text from the text box, making it empty.

  • Delete Button Functionality:

    Deletion of a selected employee record from the database in a Windows Forms application.

      //Delete
      private void button4_Click(object sender, EventArgs e)
      {
          // Check if any row is selected in the DataGridView
          if (dataGridView1.SelectedRows.Count > 0) 
          {
              // Ask the user for confirmation before deleting the selected record
              if (MessageBox.Show("Are you sure you want to delete this data?", "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
              {
                  // Get the EmployeeID (assumed to be in the first cell of the selected row) from the selected row
                  int Eno = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells[0].Value);
    
                  // Retrieve the employee object from the database that matches the selected EmployeeID
                  Employee obj = dataContext.Employees.SingleOrDefault(E => E.EmployeeID == Eno);
    
                  // If the employee is found, delete it from the database
                  dataContext.Employees.DeleteOnSubmit(obj);
    
                  // Submit the changes to the database to perform the deletion
                  dataContext.SubmitChanges();
    
                  // Reload the data in the DataGridView to reflect the deletion
                  LoadData();
              }
          }
          else
          {
              // Display a message prompting the user to select a row first if no row is selected
              MessageBox.Show("Please select a row first for deletion.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); 
          }
      }
    

    Explanation:

    • Retrieving the Employee ID:

        int Eno = Convert.ToInt32(dataGridView1.SelectedRows[0].Cells[0].Value);
      

      This line retrieves the EmployeeID from the first cell (Cells[0]) of the selected row. Eno stores this ID as an integer, which is used to identify the employee to be deleted.

    • Fetching the Employee Object:

        Employee obj = dataContext.Employees.SingleOrDefault(E => E.EmployeeID == Eno);
      

      The code queries the database to find the Employee object that matches the selected EmployeeID using LINQ.

      SingleOrDefault is used to retrieve a single employee record that matches the condition. If no match is found, obj will be null.

    • Deleting the Employee:

        dataContext.Employees.DeleteOnSubmit(obj);
        dataContext.SubmitChanges();
      

      DeleteOnSubmit marks the employee object for deletion in the database.

      SubmitChanges commits the changes to the database, effectively removing the employee record.

  • Insert Button Functionality:

    To implement the insert functionality, double-click the Insert button in the form designer to generate an event handler. Inside the generated button1_Click method, add the following code:

      private void button1_Click(object sender, EventArgs e)
      {
          Form2Insert fi = new Form2Insert(); // Create an instance of Form2Insert to handle data insertion
          fi.ShowDialog(); // Open Form2Insert as a modal dialog
          LoadData(); // Reload the data on Form1 after the insertion is complete
      }
    

    Explanation of the Insert Button Code

    1. Creating an Instance of Form2Insert:

      • Form2Insert fi = new Form2Insert();: This line creates an instance of the Form2Insert class, which is the form where users can enter new employee data.
    2. Opening Form2Insert:

      • fi.ShowDialog();: This opens the Form2Insert form as a modal dialog. A modal dialog means that the user must interact with this form before returning to the main form (Form1). The control flow pauses here until Form2Insert is closed.
    3. Reloading Data on Form1:

      • LoadData();: After the Form2Insert form is closed, control returns to this method. The LoadData() method is then called to refresh the data grid on Form1, reflecting any new data inserted via Form2Insert.
  • Submit button Functionality in Form2Insert.cs:

    This method handles the insertion of a new employee record into the database when the user clicks the "Submit" button in a Windows Forms application.

      private void button1_Click(object sender, EventArgs e)
      {
          //Create new instance
          DatabaseDataDataContext db = new DatabaseDataDataContext();
    
          //Create New Employee Object
          Employee eobj = new Employee();
    
          //Assign Values
          eobj.EmployeeID = int.Parse(textBox1.Text);
          eobj.FirstName = textBox2.Text;
          eobj.LastName = textBox3.Text;
          eobj.JobTitle = textBox4.Text;
          eobj.Salary = decimal.Parse(textBox5.Text);
          eobj.Department = textBox6.Text;
          eobj.HireDate = DateTime.Now;
    
          // Assuming you want to insert the new employee into the database
          db.Employees.InsertOnSubmit(eobj);
          db.SubmitChanges();
          MessageBox.Show("Employee added successfully!");
      }
    
    • DatabaseDataDataContext db = new DatabaseDataDataContext();: Establish a connection to the database. A new instance of the DatabaseDataDataContext class is created, which serves as the connection between the application and the database. This instance, db, allows you to interact with the database tables.

    • Create New Employee Object Employee eobj = new Employee();: A new instance of the Employee class is created. This object, eobj, represents a new record that you want to insert into the Employees table in the database.

    • Insert New Record db.Employees.InsertOnSubmit(eobj);: The InsertOnSubmit method is called on the Employees table (which is a part of the db context). This method marks the eobj object (the new employee) for insertion into the database when the changes are submitted.

    • Save Changes db.SubmitChanges();: The SubmitChanges method is called to save all the changes made to the database through the db context. This commits the insertion of the new Employee record into the Employees table in the database.

    Control return back to Form1.cs-> button1_Click.

  • Submit button Functionality in Form1.cs:

    This method is used to update an existing employee record in the database when the user clicks the "Update" button in the main form of a Windows Forms application. The code opens a new form (Form3Update) where the user can edit the details of the selected employee.

    This method is triggered when the "Update" button (button2) is clicked.

      //UpdateData:-
      private void button2_Click(object sender, EventArgs e)
      {
          if (dataGridView1.SelectedRows.Count > 0) // Check if a row is selected
          {
              DataGridViewRow selectedRow = dataGridView1.SelectedRows[0];
    
              Form3Update fu = new Form3Update(); // Initialize Form3Update //Use same form "form 2" to update the value by changing the modifier in textBox field private to internal
              fu.textBox1.ReadOnly = true;
              fu.button2.Enabled = false;
              fu.button1.Text = "Update";
              //set the data:
              fu.textBox1.Text = selectedRow.Cells[0].Value.ToString() ?? String.Empty; ;
              fu.textBox2.Text = selectedRow.Cells[1].Value.ToString() ?? String.Empty; 
              fu.textBox3.Text = selectedRow.Cells[2].Value.ToString() ?? String.Empty; 
              fu.textBox4.Text = selectedRow.Cells[3].Value.ToString() ?? String.Empty; 
              fu.textBox5.Text = selectedRow.Cells[4].Value.ToString() ?? String.Empty; 
              fu.textBox6.Text = selectedRow.Cells[5].Value.ToString() ?? String.Empty; 
              fu.textBox7.Text = selectedRow.Cells[6].Value.ToString() ?? String.Empty;
              //'dataGridView1' is DataGridView name which is taken from property
              fu.ShowDialog();
              LoadData();
          }
          else
          {
              MessageBox.Show("Please select a row first.", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); // Prompt user to select a row if none is selected
          }
      }
    
    • Form3Update fu = new Form3Update();: A new instance of the Form3Update class (which is a form designed for updating employee details) is created. This form will be used to display the selected employee's data and allow the user to modify it.

    • textBox1.ReadOnly = true;: The first textbox (textBox1) in Form3Update is made read-only, so the user cannot modify the Employee ID. This ensures that the primary key remains unchanged during the update process.

    • button2.Enabled = false;: The second button (button2) in Form3Update is disabled, which might be an additional button not used for the update process.

    • button1.Text = "Update";: The text on the first button (button1) in Form3Update is changed to "Update" to reflect the action that will be performed when clicked.

    • fu.ShowDialog();: This displays the Form3Update form as a modal dialog, meaning that the user must close this form before they can return to the main form (Form1). The user will make changes in this form and submit them.

    • LoadData();: After the user closes the update form, the LoadData() method is called to refresh the DataGridView in the main form, reflecting any changes made to the employee's data.


Calling Stored Procedure using LINQ:

  • When you open the Server Explorer in Visual Studio, under the Data Connections node, you will see a list of available databases. Inside each database connection, you'll find a folder named Stored Procedures. This folder may be empty if you haven't created any stored procedures yet.

Creating a Stored Procedure:

  1. Right-click on the Stored Procedures folder under your database in the Object Explorer.

  2. Select New Stored Procedure.

  3. A new query window will open with a template for creating a stored procedure.

SQL Code to Create a Stored Procedure:

Let's create a stored procedure that returns all records from the Employee table:

CREATE PROCEDURE Employee_Select
AS
SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate
FROM Employee Order By EmployeeID;

--Or
CREATE PROCEDURE Employee_Select
AS
BEGIN
    SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate
    FROM Employee
    ORDER BY EmployeeID;
END

The code you provided is SQL code, specifically Transact-SQL (T-SQL), which is used for writing queries and stored procedures in SQL Server.

  • CREATE PROCEDURE: This is a SQL command used to create a stored procedure in a SQL Server database.

  • AS and BEGIN ... END: These keywords define the body of the stored procedure. The first version of this code omits BEGIN ... END, which is optional when i have only one SQL statement, but the second version includes them to clearly define the start and end of the procedure's body.

  • SELECT ... FROM ... ORDER BY: This is a SQL query that selects specific columns (EmployeeID, FirstName, LastName, etc.) from the Employee table and orders the results by EmployeeID.

  • After writing the SQL code, right-click on the dbo.Procedure.sql page, then select Execute or press Ctrl + Shift + E.If any errors occur, they will be displayed at the bottom of the window. If there are no errors, a success message will be shown.

  • There’s no need to save the command because the procedure is created directly on the database server.

  • If you refresh the Server Explorer, you will find the stored procedure listed. In this example, it will be named Employee_Select.

Calling the Stored Procedure using LINQ:

  1. Double-click on the .dbml file in your project.

  2. Drag and drop the Employee_Select stored procedure from the Server Explorer onto the right-hand side of the .dbml design surface.

  • This action will automatically generate a method named Employee_Select() in the DatabaseDataDataContext class, which is defined in the DatabaseData.designer.cs file.

      public ISingleResult<Employee_SelectResult> Employee_Select()
      {
          IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())));
          return ((ISingleResult<Employee_SelectResult>)(result.ReturnValue));
      }
    

  • The method Employee_Select() returns an ISingleResult<Employee_SelectResult>, where Employee_SelectResult is a class generated by LINQ to SQL.

  • The Employee_SelectResult class contains properties corresponding to the columns selected in the stored procedure (EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate).

  • ISingleResult<Employee_SelectResult> is a type in LINQ to SQL that represents the result of executing a stored procedure or a query that returns a sequence of records.

    1. ISingleResult<T>: ISingleResult is an interface in LINQ to SQL, specifically in the System.Data.Linq namespace. It is used to represent the result of a query or stored procedure that returns a collection of records (rows). T is the type of the individual records in that collection.

      ISingleResult is like a table of Employee_SelectResult in the database, similar to Table<Employee>, which is just a table of Employee.

    2. Employee_SelectResult: This is a class generated by LINQ to SQL when you drag and drop a stored procedure into the .dbml design surface. The class Employee_SelectResult contains properties that match the columns returned by the Employee_Select stored procedure. For example, if the stored procedure returns columns like EmployeeID, FirstName, LastName, etc., the Employee_SelectResult class will have properties corresponding to these columns.

    ISingleResult<Employee_SelectResult> represents a collection of Employee_SelectResult objects, where each object corresponds to a row returned by the Employee_Select stored procedure.

    ISingleResult ensures that you can enumerate over the result set, typically using a foreach loop, to access each Employee_SelectResult object in the collection.

Difference Between Employee Class and Employee_SelectResult Class:

  • Employee Class: Represents the entire Employee table. It contains properties for all the columns in the table, so when you use this class, you automatically retrieve all the columns.

  • Employee_SelectResult Class: Represents the result of the Employee_Select stored procedure. This class is similar to the Employee class but is specifically tailored to match the columns returned by the stored procedure. This allows you to specify and retrieve only the columns you need from the Employee table, offering more control over the data you work with.

  • In summary, while the Employee class automatically includes all columns from the Employee table, the Employee_SelectResult class allows you to retrieve only the columns specified in your stored procedure, giving you greater flexibility in managing the data and you can write any type of SQL query like:

      CREATE PROCEDURE Employee_SelectBySalary
          @MinimumSalary DECIMAL(18, 2)
      AS
      BEGIN
          SELECT EmployeeID, FirstName, Salary
          FROM Employee
          WHERE Salary > @MinimumSalary
          ORDER BY Salary DESC;
      END
    

Calling the Employee_Select() method and displaying the results in a DataGridView:

  • Create a new form named Form1SQL with a DataGridView. Double-click the form (not the DataGridView). When you do this, the Form1SQL.cs page will open. Then, import the using System.Data.Linq; namespace. write this code:-

      private void Form1SQL_Load(object sender, EventArgs e)
      {
          DatabaseDataDataContext db = new DatabaseDataDataContext();
          ISingleResult<Employee_SelectResult> tab = db.Employee_Select();
          dataGridView1.DataSource = tab;
      }
    

    In the future, if the Employee_Select() method needs a parameter, we will be able to pass it.

    In the case of dc.Employee_Select, we use a predefined property, but in dc.Employee_Select(), we use a stored procedure to perform this.

    Update Program.cs to Run Form1SQL: Modify the Main Method in Program.cs. Change the Application.Run line to use Form1SQL instead of the default form.

      using System;
      using System.Windows.Forms;
    
      namespace CRUD_WindowsForm
      {
          static class Program
          {
              [STAThread]
              static void Main()
              {
                  Application.EnableVisualStyles();
                  Application.SetCompatibleTextRenderingDefault(false);
                  Application.Run(new Form1SQL());
              }
          }
      }
    

    Summary:

    • Form Creation: You created Form1SQL with a DataGridView and implemented the Form1SQL_Load event to load data.

    • Loading Data: In Form1SQL_Load, you instantiated DatabaseDataDataContext, called Employee_Select(), and set the DataSource of dataGridView1 to the result.

    • Running the Form: You updated Program.cs to run Form1SQL instead of the default form.

    This setup ensures that when Form1SQL is loaded, it will call the Employee_Select() stored procedure, retrieve the data, and display it in the DataGridView.

If you want to retrieve any type of data according to your requirements, you can do so by changing the query of Employee_SelectResult . For example, if I want the data based on the department name:

CREATE PROCEDURE Employee_Select(@Department Varchar(50) = Null)
AS
Begin
if @Department is Null
    SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate FROM Employee Order By EmployeeID;
Else
    SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate FROM Employee Where Department = @Department Order By EmployeeID;
End;

/* Old Query:

CREATE PROCEDURE Employee_Select
AS
SELECT EmployeeID, FirstName, LastName, JobTitle, Salary, Department, HireDate
FROM Employee Order By EmployeeID;*/

Follow the same process: delete the current Employee_Select available in the .dbml file, then re-execute the DatabaseData.designer.cs and drag and drop it on the right side of the .dbml file. Now you will see it takes a parameter.

private void Form1SQL_Load(object sender, EventArgs e)
{
      DatabaseDataDataContext db = new DatabaseDataDataContext();
      ISingleResult<Employee_SelectResult> tab = db.Employee_Select();
      dataGridView1.DataSource = tab;
}

Now hear db.Employee_Select(), if you pass null, you get all values. If you pass a department name, you get data for that department. If you don't pass anything, you get an error.

Here is the code for the DatabaseData.designer.cs file:

public ISingleResult<Employee_SelectResult> Employee_Select([global::System.Data.Linq.Mapping.ParameterAttribute(Name="Department", DbType="VarChar(50)")] string department)
{
    IExecuteResult result = this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), department);
    return ((ISingleResult<Employee_SelectResult>)(result.ReturnValue));
}

Now it's taking a parameter.

If the stored procedure is parameterized, then the method becomes parameterized. If the stored procedure is non-parameterized, then the DatabaseData.designer.cs method is also non-parameterized.


How to write a query on a database using SQL:

Create table to understande:

Employee table:

EmployeeIDFirstNameLastNameJobTitleSalaryDepartmentHireDate
1JohnDoeSoftware Engineer75000.00IT2022-01-15
2JaneSmithData Analyst68000.00IT2021-03-22
3MichaelJohnsonProject Manager85000.00Operations2020-08-30
4EmilyDavisHR Specialist60000.00Human Resources2019-11-05
5DavidBrownSenior Developer90000.00IT2018-07-17
6LindaWilsonMarketing Manager78000.00Marketing2023-04-12
7RobertTaylorAccountant67000.00Finance2021-12-01
8MaryLeeCustomer Support52000.00Support2020-06-15
9JamesMartinDevOps Engineer83000.00IT2017-05-23
10PatriciaAndersonBusiness Analyst72000.00Operations2019-02-14

Department table:

CREATE TABLE Department (
    DepartmentID INT PRIMARY KEY,
    DepartmentName VARCHAR(50) NOT NULL,
    Manager VARCHAR(50)
);
INSERT INTO Department (DepartmentID, DepartmentName, Manager) VALUES
(1, 'IT', 'Alice Cooper'),
(2, 'Operations', 'Bob Stevens'),
(3, 'Human Resources', 'Catherine Green'),
(4, 'Marketing', 'Diana Prince'),
(5, 'Finance', 'Edward Norton'),
(6, 'Support', 'Fiona White');
DepartmentIDDepartmentNameManager
1ITAlice Cooper
2OperationsBob Stevens
3Human ResourcesCatherine Green
4MarketingDiana Prince
5FinanceEdward Norton
6SupportFiona White

Both tables are in the same database Company in SQL Server. Configure the database in Server Explorer in Visual Studio. Drag and drop both tables into the .dbml file on the left side. Then, create the Form2SQL.cs file.

Double-click on the form, and a load method will be created:

private void Form2SQL_Load(object sender, EventArgs e){}

Change the Program.cs file code to run Form2SQL().

Application.Run(new Form2SQL());

Writing the SQL query:

  • Syntac:-

Select * | <collist> form <table> as <allas> [<clauses>]
  • Sequence for writing the SQL query:

    Clauses:

    • Where

    • Group By

    • Having

    • Order By

Writing the LINQ query:

  • Syntax:

      From <alias> in <table> [<clauses>] select <alias> | new {<list of columns/collist>}
    
  • Clauses:

    • Where

    • Group By

    • Order By

But you can use Having clauses in LINQ query.

  • Example: Retrive all data from database

      From E in dc.Employees select E;
    

    To strore the data:

      var tabl = From E in dc.Employees select E;
    

Example of LINQ:

  1. Get all IT department employees:

     var tab = from E in dc.Employees where E.Department == "IT" select E;
    
     private void Form2SQL_Load(object sender, EventArgs e)
     {
         DatabaseDataDataContext db = new DatabaseDataDataContext();
         var tab = from E in db.Employees select E;
         dataGridView1.DataSource = tab;
     }
    

  2. Get EmployeeID, FirstName, LastName, JobTitle and Salary of all IT department employees:

     var tab = from E in db.Employees where E.Department == "IT" 
     select new { E.EmployeeID, E.FirstName, E.LastName, E.JobTitle, E.Salary };
    
     private void Form2SQL_Load(object sender, EventArgs e)
     {
         DatabaseDataDataContext db = new DatabaseDataDataContext();
    
         var tab = from E in db.Employees where E.Department == "IT"
         select new { E.EmployeeID, E.FirstName, E.LastName, E.JobTitle, E.Salary};
    
         dataGridView1.DataSource = tab.ToList();
     }
    

  3. Create filter by Department:

    • Add comboBox in form. And add this code in Form Load:

        //Form2SQL.cs
        using System;
        using System.Data;
        using System.Linq;
        using System.Windows.Forms;
      
        namespace CRUD_WindowsForm
        {
            public partial class Form2SQL : Form
            {
                DatabaseDataDataContext db;
                public Form2SQL()
                {
                    InitializeComponent();
                }
      
                //Form load method
                private void Form2SQL_Load(object sender, EventArgs e)
                {
                    db = new DatabaseDataDataContext();
      
                    var dep = from E in db.Employees select new { E.Department };
                    //comboBox1.DataSource = dep; //comboBox1.DataSource = dep; This shows all departments, but the problem is it comes in object form like { Department = "IT" }, and I want it to show only "IT".
                    comboBox1.DataSource = dep.Distinct(); //This shows all departments without duplicates, but the problem is it comes in object form like { Department = "IT" }, and I want it to show only "IT".
                    comboBox1.DisplayMember = "Department"; //This shows all departments in a readable form like "IT"
      
                    dataGridView1.DataSource = from E in db.Employees select E;
      
                }
      
                //Combo box method
                private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
                {
                    dataGridView1.DataSource = from E in db.Employees where E.Department == comboBox1.Text select E; //Filter the data and show in dataGridView1
                }
            }
        }
      

      Explanation:

      • DatabaseDataDataContext db; Database Context (db): The DatabaseDataDataContext object (db) is declared as a class-level variable.

      • Constructor: The Form2SQL constructor initializes the form components but doesn't do anything specific for the database until the form loads.

      • Form Load (Form2SQL_Load) : When the form is loaded, the DatabaseDataDataContext object is initialized db = new DatabaseDataDataContext();, establishing a connection to the database .

      • Initial Data Load (dataGridView1.DataSource = from E in db.Employees select E;) : This line sets the DataGridView (dataGridView1) data source to all rows from the Employees table. The select E query retrieves all columns for each employee .

      • Department Query (dep) var dep = from E in db.Employees select new { E.Department };: This LINQ query retrieves all department names from the Employees table. The query returns an anonymous object with a single property Department for each row.

      • The Distinct() method ensures that each department name appears only once, eliminating duplicates.

      • DisplayMember (comboBox1.DisplayMember = "Department";): The DisplayMember property is set to "Department", which tells the ComboBox to display just the department names, not the full anonymous object { Department = "IT" }. The comment notes an issue with duplicates appearing if Distinct() isn't used correctly.

      Event Handling:

      private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
      {
          dataGridView1.DataSource = from E in db.Employees where E.Department == comboBox1.Text select E;
      }
      
      • ComboBox Selection Change (comboBox1_SelectedIndexChanged):

        • When the user selects a department from the ComboBox, the SelectedIndexChanged event triggers.

        • The DataGridView is then filtered to show only those employees whose Department matches the selected value in comboBox1. The query retrieves all columns (select E) for the matching employees.

    • Output: If you do not see the output, click on this link https://cdn.hashnode.com/res/hashnode/image/upload/v1723745503915/611620fb-4b26-4427-8e4f-b426ffe79266.gif?auto=format,compress&gif-q=60&format=webm.

  4. Get data ordered by Salary:

     dataGridView1.DataSource = from E in db.Employees 
     orderby E.Salary select E;
    
  5. Get data ordered by FirstName in descending order:

     dataGridView1.DataSource = from E in db.Employees 
     orderby E.FirstName descending select E;
    
  6. Get required columns:

     dataGridView1.DataSource = from E in db.Employees 
     select new { E.EmployeeID, E.FirstName, E.Department, E.Salary};
    
  7. Change the column names or alias the names First_Name = E.FirstName:

     dataGridView1.DataSource = from E in db.Employees 
     select new { E.EmployeeID, First_Name = E.FirstName, E.Department, E.Salary};
    
  8. Get the number of employees in each Department:

     //In SQL:
     Select Department, EmployeeCount = count(*) from Emp Group By Department;
     //In LINQ:
     dataGridView1.DataSource = from E in db.Employees
                                group E by E.Department into deptGroup
                                select new
                                {
                                    Department = deptGroup.Key,
                                    EmployeeCount = deptGroup.Count()
                                };
    

    The into keyword allows you to give a name to the grouped result and continue querying it.

    deptGroup is name which represents each group of employees that share the same department.

    deptGroup: After grouping, each deptGroup represents a group of employees in a specific department. It's a collection of all employees who have the same department value.

    The Key property is used to access the value by which the grouping was performed.

    deptGroup.Key: This refers to the department name (e.g., "IT", "HR") that the group is based on.

    Count() is an aggregate function in LINQ. We can use all aggregate functions like Max, Min, Sum, Avg, and Count.

  9. Get the department which have greater than 3 employees :

     //In SQL:
     Select Department, EmployeeCount = count(*) from Emp Group By Department Having Count(*) > 5;
    

    In LINQ: LINQ doesn't have HAVING clauses. So, how do you achieve this? In LINQ, when you use WHERE clauses before GROUP BY, it works like a WHERE clause. But if you use WHERE clauses after GROUP BY, it works like a HAVING clause.

     //In LINQ
     dataGridView1.DataSource = from E in db.Employees
                                group E by E.Department into deptGroup
                                where deptGroup.Count() > 5
                                select new
                                {
                                    Department = deptGroup.Key,
                                    EmployeeCount = deptGroup.Count()
                                };
    
  10. Use multiple clauses:

    //In SQL:
    Select Dept, Count = Count(*) From Employees Where Department = "IT" Group By Dept; 
    //In LINQ:
    dataGridView1.DataSource = from E in db.Employees
                               where E.Department == "IT"
                               group E by E.Department into D
                               select new { Dept = D.Key, Count = D.Count() };
    
    //In SQL:
    Select Dept, Count = Count(*) From Employees Where Department = "IT" Group By Dept Having Count(*)>1; 
    //In LINQ:
    dataGridView1.DataSource = from E in db.Employees
                               where E.Department == "IT"
                               group E by E.Department into D
                               where D.Count() > 1
                               select new { Dept = D.Key, Count = D.Count() };
    

    Arrange this data in decinding order of department number:

    //In SQL:
    Select Dept, Count = Count(*) From Employees Where Department = "IT" Group By Dept Having Count(*)>1 Order By Dept DESC; 
    //In LINQ:
    dataGridView1.DataSource = from E in db.Employees
                               where E.Department == "IT"
                               group E by E.Department into D
                               where D.Count() > 1 
                               orderby D.key descending
                               select new { Dept = D.Key, Count = D.Count() };
    

Thank you for completing the C# programming language course with a focus on LINQ concepts. Your dedication to mastering this powerful tool will undoubtedly enhance your coding efficiency and data manipulation skills. Keep up the great work!